The story so far

Last month I was trying to get SMART data from a disk drive. The documentation I could find worked great for one disk drive however when two drives were in use I had a problem linking the data returned from the two WMI select queries SELECT * FROM Win32_DiskDrive and Select * from MSStorageDriver_FailurePredictData, as I cannot assume the data is in the same order.

I used the index property in the Win32_DiskDrive data and everything worked fine for my machine with two disks.

Three disks

As is often the case with computers everything can appear to be fine and it is possible to only work on my machine.

I moved to a machine with three drives and I got the same problem, the SMART data was mismatched with the Win32_DiskDrive data.

After some research I found this article I adopted the same approach that others have, I used the InstanceName returned from the SMART data WMI queries and related that to the PNPDeviceID returned from the Win32_DiskDrive. However its not as simple as that. The PNPDeviceID value was SCSI\DISK&VEN_TOSHIBA&PROD_MQ01ABD100\4&229DE8AE&0&000000 and the value of the InstanceName was SCSI\Disk&Ven_TOSHIBA&Prod_MQ01ABD100\4&229de8ae&0&000000_0, note the difference in case and the additional _0

Its not completely ideal but the code I ended up with is like this, the key piece is that I used StartsWith and ignore the case when comparing.

public Dictionary<int, HDD> GetAllDisks()
{
	// retrieve list of drives on computer (this will return both HDD's and CDROM's and Virtual CDROM's)                    
	var dicDrives = new Dictionary<int, HDD>();
	var wdSearcher = new ManagementObjectSearcher("SELECT * FROM Win32_DiskDrive");

	// extract model and interface information
	int iDriveIndex = 0;
	foreach (ManagementObject drive in wdSearcher.Get())
	{
		//LogWmiData(drive);

		var hdd = new HDD();
		hdd.Id = drive["DeviceId"].ToString().Trim();
		hdd.Index = Convert.ToInt32(drive["Index"].ToString().Trim());
		hdd.Model = drive["Model"].ToString().Trim();
		_logger.Write($"disk drive {hdd.Index} {hdd.Id} {hdd.Model}");
		hdd.Type = drive["InterfaceType"].ToString().Trim();
		hdd.PnpDeviceId = drive["PNPDeviceID"].ToString().Trim();
		if (drive["Availability"] != null)
		{
			hdd.Availability = (Availability)(drive["Availability"]);
		}
		if (drive["Capabilities"] != null)
		{
			hdd.Capabilities = (UInt16[])(drive["Capabilities"]);
		}
		dicDrives.Add(iDriveIndex, hdd);
		iDriveIndex++;
	}
	return dicDrives;
}

public string GetProperty(ManagementObject obj, string key)
{
	PropertyData data = obj.Properties[key];
	if (data == null || data.Value == null)
	{
		return "N/A";
	}
	return data.Value.ToString();
}

public bool isDevice(HDD drive, string instanceName)
{
	return instanceName.StartsWith(drive.PnpDeviceId, StringComparison.InvariantCultureIgnoreCase);
}

public void GetSMARTInformation(HDD drive)
{
	// this is the actual physical drive number
	_logger.Write($"Drive Number {drive.Index} Drive {drive.Id}, {drive.Model}, PNP {drive.PnpDeviceId}");

	// get wmi access to hdd 
	var searcher = new ManagementObjectSearcher("Select * from Win32_DiskDrive");
	searcher.Scope = new ManagementScope(@"\root\wmi");

	// check if SMART reports the drive is failing
	searcher.Query = new ObjectQuery("Select * from MSStorageDriver_FailurePredictStatus");
	foreach (ManagementObject thisDrive in searcher.Get())
	{
		_logger.Write($"Instance Name 1 {GetProperty(thisDrive,"InstanceName")}");
		if (isDevice(drive, GetProperty(thisDrive, "InstanceName")))
		{
			drive.IsOK = (bool)thisDrive.Properties["PredictFailure"].Value == false;
		}
	}

	// retrieve attribute flags, value worst and vendor data information
	searcher.Query = new ObjectQuery("Select * from MSStorageDriver_FailurePredictData");
	foreach (ManagementObject data in searcher.Get())
	{
		_logger.Write($"Instance Name 2 {GetProperty(data, "InstanceName")}");
		if (isDevice(drive, GetProperty(data, "InstanceName")))
		{
			Byte[] bytes = (Byte[])data.Properties["VendorSpecific"].Value;
			for (int i = 0; i < 30; ++i)
			{
				int id = 0;
				try
				{
					id = bytes[i * 12 + 2];

					int flags = bytes[i * 12 + 4]; // least significant status byte, +3 most significant byte, but not used so ignored.
													//bool advisory = (flags & 0x1) == 0x0;
					bool failureImminent = (flags & 0x1) == 0x1;
					//bool onlineDataCollection = (flags & 0x2) == 0x2;

					int value = bytes[i * 12 + 5];
					int worst = bytes[i * 12 + 6];
					int vendordata = BitConverter.ToInt32(bytes, i * 12 + 7);
					if (id == 0) continue;

					//_logger.Write($"SMART Data: {id} {vendordata}");

					Smart attr = drive.Attributes[id];
					attr.Current = value;
					attr.Worst = worst;
					attr.Data = vendordata;
					attr.IsOK = failureImminent == false;
					attr.IsPopulatedFromWmi = true;
				}
				catch
				{
					// given key does not exist in attribute collection (attribute not in the dictionary of attributes)
					_logger.Write($"SMART Key Not found {id}");
				}
			}
		}
	}

	// retrieve threshold values foreach attribute
	searcher.Query = new ObjectQuery("Select * from MSStorageDriver_FailurePredictThresholds");
	foreach (ManagementObject data in searcher.Get())
	{
		_logger.Write($"Instance Name 3 {GetProperty(data, "InstanceName")}");
		if (isDevice(drive, GetProperty(data, "InstanceName")))
		{
			Byte[] bytes = (Byte[])data.Properties["VendorSpecific"].Value;
			for (int i = 0; i < 30; ++i)
			{
				try
				{

					int id = bytes[i * 12 + 2];
					int thresh = bytes[i * 12 + 3];
					if (id == 0) continue;

					var attr = drive.Attributes[id];
					attr.Threshold = thresh;
				}
				catch
				{
					// given key does not exist in attribute collection (attribute not in the dictionary of attributes)
				}
			}
		}
	}
}

This works fine, until I find a machine with four disks.