|
18 | 18 | SEVERITY_CRITICAL Maps to STATE_YES. |
19 | 19 | SEVERITY_WARNING Maps to STATE_MAYBE. |
20 | 20 | SEVERITY_NONE Maps to STATE_NO. |
| 21 | +
|
| 22 | +Vendor-specific attribute handling |
| 23 | +----------------------------------- |
| 24 | +Seagate and some other vendors pack compound data into the raw 48-bit value |
| 25 | +for certain ATA attributes. The most common case is attribute 188 |
| 26 | +(Command_Timeout) where the full raw value can appear as hundreds of |
| 27 | +billions when the actual timeout count is only in the lower bytes. |
| 28 | +
|
| 29 | +References: |
| 30 | + - Backblaze Hard Drive Stats methodology |
| 31 | + - smartmontools wiki on vendor-specific raw values |
21 | 32 | """ |
22 | 33 |
|
23 | 34 | from __future__ import annotations |
|
67 | 78 | "Command_Timeout": "Command Timeout", |
68 | 79 | } |
69 | 80 |
|
| 81 | +# --------------------------------------------------------------------------- |
| 82 | +# Vendor-specific raw value decoding |
| 83 | +# --------------------------------------------------------------------------- |
| 84 | +# Several drive vendors — most notably Seagate, but also OEM/rebadged drives |
| 85 | +# (e.g., OOS-prefixed models) — pack compound data into the 48-bit raw value |
| 86 | +# for certain ATA attributes. Command_Timeout (attribute 188) is the most |
| 87 | +# common case: the full raw value can be hundreds of billions when the actual |
| 88 | +# timeout count is only in the lower 16 bits. |
| 89 | +# |
| 90 | +# Example: a raw value of 940,612,190,430 (0x00DB00DB00DE) on a Seagate |
| 91 | +# ST12000NM0558 breaks down as three 16-bit counters packed together. The |
| 92 | +# meaningful error count is in the lowest 16 bits: 0x00DE = 222. |
| 93 | +# |
| 94 | +# Detection: rather than relying solely on vendor identification (which fails |
| 95 | +# for OEM/rebadged drives), we detect compound encoding by the value itself. |
| 96 | +# No drive should have more than 65,535 actual command timeouts and still be |
| 97 | +# responding to smartctl. A raw value >0xFFFF for Command_Timeout is almost |
| 98 | +# certainly compound-encoded. |
| 99 | +# |
| 100 | +# References: |
| 101 | +# - Backblaze Hard Drive Stats methodology (uses lower 16 bits) |
| 102 | +# - smartmontools wiki on vendor-specific raw values |
| 103 | + |
| 104 | +# Seagate model prefixes and identifiers for vendor detection. |
| 105 | +_SEAGATE_PREFIXES: tuple[str, ...] = ("st", "seagate") |
| 106 | + |
| 107 | + |
| 108 | +def _is_seagate(model: str) -> bool: |
| 109 | + """Return True if the drive model string indicates a Seagate drive.""" |
| 110 | + lower = model.lower() |
| 111 | + return any(lower.startswith(p) for p in _SEAGATE_PREFIXES) or "seagate" in lower |
| 112 | + |
| 113 | + |
| 114 | +def _decode_command_timeout(raw_value: int) -> int: |
| 115 | + """Extract the actual timeout count from a Command_Timeout raw value. |
| 116 | +
|
| 117 | + Seagate and OEM drives pack three 16-bit counters into the 48-bit raw |
| 118 | + value. The lowest 16 bits hold the meaningful error count. Values |
| 119 | + above 0xFFFF are always compound-encoded — a drive with 65K+ real |
| 120 | + timeouts would be unresponsive. |
| 121 | + """ |
| 122 | + if raw_value > 0xFFFF: |
| 123 | + return raw_value & 0xFFFF |
| 124 | + return raw_value |
| 125 | + |
| 126 | + |
| 127 | +def _decode_ata_raw(attr_name: str, raw_value: int) -> int: |
| 128 | + """Decode the actual error count from a raw ATA attribute value. |
| 129 | +
|
| 130 | + For most attributes, the raw value IS the count. For attributes with |
| 131 | + known compound encoding (e.g., Command_Timeout), we extract the real |
| 132 | + counter from the appropriate byte position. |
| 133 | + """ |
| 134 | + if attr_name == "Command_Timeout": |
| 135 | + return _decode_command_timeout(raw_value) |
| 136 | + return raw_value |
| 137 | + |
70 | 138 |
|
71 | 139 | def _has_usable_smart_data(smart_data: dict[str, Any]) -> bool: |
72 | 140 | """Return True if the SMART data dict contains anything we can evaluate. |
@@ -192,16 +260,21 @@ def evaluate_attention( |
192 | 260 | if not isinstance(raw_value, (int, float)) or raw_value <= 0: |
193 | 261 | continue |
194 | 262 |
|
| 263 | + # Decode compound-encoded attributes (e.g., Seagate Command_Timeout). |
| 264 | + decoded = _decode_ata_raw(name, int(raw_value)) |
| 265 | + |
195 | 266 | label = _CRITICAL_ATA.get(name) |
196 | 267 | if label and label not in seen_labels: |
197 | | - critical_reasons.append(f"{label}: {int(raw_value)} (expected 0)") |
198 | | - seen_labels.add(label) |
| 268 | + if decoded > 0: |
| 269 | + critical_reasons.append(f"{label}: {decoded} (expected 0)") |
| 270 | + seen_labels.add(label) |
199 | 271 | continue |
200 | 272 |
|
201 | 273 | label = _WARNING_ATA.get(name) |
202 | 274 | if label and label not in seen_labels: |
203 | | - warning_reasons.append(f"{label}: {int(raw_value)} (expected 0)") |
204 | | - seen_labels.add(label) |
| 275 | + if decoded > 0: |
| 276 | + warning_reasons.append(f"{label}: {decoded} (expected 0)") |
| 277 | + seen_labels.add(label) |
205 | 278 |
|
206 | 279 | return _assemble(critical_reasons, warning_reasons) |
207 | 280 |
|
|
0 commit comments