Skip to content

Commit e7d52af

Browse files
committed
Fix Seagate compound-encoded Command Timeout causing false MAYBE alerts
Seagate and OEM drives pack three 16-bit counters into the 48-bit raw value for SMART attribute 188. The integration was comparing the full compound value against zero, triggering false warnings on every Seagate. Now decodes to lower 16 bits when raw value exceeds 0xFFFF. Applies to all vendors — no drive should have 65K+ real command timeouts. Fixes: #3
1 parent e09b17e commit e7d52af

2 files changed

Lines changed: 89 additions & 4 deletions

File tree

custom_components/smart_sniffer/attention.py

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@
1818
SEVERITY_CRITICAL Maps to STATE_YES.
1919
SEVERITY_WARNING Maps to STATE_MAYBE.
2020
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
2132
"""
2233

2334
from __future__ import annotations
@@ -67,6 +78,63 @@
6778
"Command_Timeout": "Command Timeout",
6879
}
6980

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+
70138

71139
def _has_usable_smart_data(smart_data: dict[str, Any]) -> bool:
72140
"""Return True if the SMART data dict contains anything we can evaluate.
@@ -192,16 +260,21 @@ def evaluate_attention(
192260
if not isinstance(raw_value, (int, float)) or raw_value <= 0:
193261
continue
194262

263+
# Decode compound-encoded attributes (e.g., Seagate Command_Timeout).
264+
decoded = _decode_ata_raw(name, int(raw_value))
265+
195266
label = _CRITICAL_ATA.get(name)
196267
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)
199271
continue
200272

201273
label = _WARNING_ATA.get(name)
202274
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)
205278

206279
return _assemble(critical_reasons, warning_reasons)
207280

custom_components/smart_sniffer/sensor.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,18 @@ def _extract_attribute(drive_data: dict[str, Any], key: str) -> Any | None:
312312
return int(m.group(1))
313313
# Fallback: low 16 bits hold current temp.
314314
return raw_value & 0xFFFF
315+
316+
# Command_Timeout (attribute 188): some vendors — notably
317+
# Seagate and OEM drives — pack compound data into the
318+
# 48-bit raw value. The actual timeout count is in the
319+
# lower 16 bits. Values above 0xFFFF are always compound.
320+
if (
321+
key == "command_timeout"
322+
and isinstance(raw_value, int)
323+
and raw_value > 0xFFFF
324+
):
325+
return raw_value & 0xFFFF
326+
315327
return raw_value
316328
return raw
317329

0 commit comments

Comments
 (0)