package main import ( "fmt" "strings" ) // wearAttr maps vendor SSD-life attribute IDs to a source label. For all of // these the normalized VALUE expresses "% life remaining". var wearAttr = []struct { id int src string }{ {173, "Micron/ID173"}, {202, "Intel/ID202"}, {231, "Intel/ID231"}, {177, "Samsung/ID177"}, {233, "Generic/ID233"}, } // ataAttr is one parsed row of an ATA SMART attribute table, shared by the JSON // and text paths. raw is the attribute's raw counter; value/worst are the // vendor-normalized current/worst values. type ataAttr struct { value *int worst *int whenFailed string raw *int } // attrRaw returns the raw counter for attribute id, or nil when absent. func attrRaw(attrs map[int]ataAttr, id int) *int { if a, ok := attrs[id]; ok { return a.raw } return nil } // attrVal returns the normalized current value for attribute id, or nil. func attrVal(attrs map[int]ataAttr, id int) *int { if a, ok := attrs[id]; ok { return a.value } return nil } // attrsFailed reports whether any attribute is flagged failed now or in the // past — the basis for the PASSED_BY_ATTR/FAILED verdict when no explicit // overall health result is available. func attrsFailed(attrs map[int]ataAttr) bool { for _, a := range attrs { wf := strings.ToLower(a.whenFailed) if wf == "now" || wf == "past" { return true } } return false } // applyAtaCounters maps the parsed ATA attribute table onto the defect, wear, // reserve-block, and host-write fields shared by the JSON and text paths. Power- // on hours fall back to attribute 9 only when not already set from a dedicated // field. Path-specific fallbacks (power-cycle/temperature on text, SCSI/NVMe on // JSON) stay with their callers. func applyAtaCounters(attrs map[int]ataAttr, d *Drive) { d.Reallocated = attrRaw(attrs, 5) d.ReallocatedEvents = attrRaw(attrs, 196) d.Pending = attrRaw(attrs, 197) d.Uncorrectable = attrRaw(attrs, 198) d.UdmaCrc = attrRaw(attrs, 199) d.ReportedUncorrect = attrRaw(attrs, 187) d.RuntimeBadBlocks = attrRaw(attrs, 183) d.EndToEnd = attrRaw(attrs, 184) if d.PowerOnHours == nil { d.PowerOnHours = attrRaw(attrs, 9) } // Wear (vendor-normalized; VALUE = % remaining). for _, w := range wearAttr { if v := attrVal(attrs, w.id); v != nil { d.WearPctRemaining = v if a, ok := attrs[w.id]; ok { d.WearPctWorst = a.worst } d.WearSrc = w.src d.WearPctConsumed = pInt(100 - *v) break } } // Micron ID180 reserve blocks (VALUE = % remaining) and ID246 host writes. if a, ok := attrs[180]; ok { d.UnusedReservePct = a.value } if lba := attrRaw(attrs, 246); lba != nil && *lba > 0 { d.HostWrittenTB = pF(float64(*lba) * 512.0 / 1e12) } } // parseSmartJSON fills d from a smartctl -j object (ATA/SATA, SAS/SCSI, or NVMe). func parseSmartJSON(j map[string]interface{}, d *Drive) { if j == nil { return } d.Model = first(jStr(j, "model_name"), jStr(j, "scsi_model_name")) d.Serial = jStr(j, "serial_number") d.Transport = jStr(j, "scsi_transport_protocol", "name") d.Firmware = first(jStr(j, "firmware_version"), jStr(j, "scsi_revision"), jStr(j, "revision")) if cap := jInt(j, "user_capacity", "bytes"); cap != nil && *cap > 0 { d.Capacity = fmt.Sprintf("%.2f TB", float64(*cap)/1e12) } switch rr := jInt(j, "rotation_rate"); { case rr != nil && *rr == 0: d.Rotation = "SSD" case rr != nil: d.Rotation = fmt.Sprintf("%d rpm", *rr) default: d.Rotation = "SSD" // Absent rotation_rate: assume SSD; NVMe is corrected just below. } if strings.Contains(strings.ToLower(jStr(j, "device", "type")), "nvme") || jObj(j, "nvme_smart_health_information_log") != nil { d.Rotation = "NVMe" } d.PowerOnHours = jInt(j, "power_on_time", "hours") d.PowerCycleCount = jInt(j, "power_cycle_count") d.TempC = jInt(j, "temperature", "current") // ---- ATA attribute table ---- attrs := map[int]ataAttr{} if table, ok := jLeaf(j, "ata_smart_attributes", "table").([]interface{}); ok { for _, it := range table { a, ok := it.(map[string]interface{}) if !ok { continue } id := jInt(a, "id") if id == nil { continue } at := ataAttr{ value: jInt(a, "value"), worst: jInt(a, "worst"), whenFailed: jStr(a, "when_failed"), } // Prefer the leading integer of raw.string (raw.value overflows // for some attributes); fall back to raw.value. if rs := jStr(a, "raw", "string"); rs != "" { if n, ok := firstInt(rs); ok { at.raw = &n } } if at.raw == nil { at.raw = jInt(a, "raw", "value") } attrs[*id] = at } } // ---- SMART health verdict ---- if passed := jBoolPtr(j, "smart_status", "passed"); passed != nil { if *passed { d.SmartHealth = "PASSED" } else { d.SmartHealth = "FAILED" } } else if len(attrs) > 0 { if attrsFailed(attrs) { d.SmartHealth = "FAILED" } else { d.SmartHealth = "PASSED_BY_ATTR" } } else { d.SmartHealth = "UNKNOWN" } // Defect, wear, reserve, and host-write fields shared with the text path. applyAtaCounters(attrs, d) // ---- SCSI/SAS endurance + grown defect list ---- if d.WearPctRemaining == nil { if pu := jInt(j, "scsi_percentage_used_endurance_indicator"); pu != nil { d.WearPctConsumed = pu d.WearPctRemaining = pInt(100 - *pu) d.WearSrc = "SCSI/endurance" } } if grown := jInt(j, "scsi_grown_defect_list"); grown != nil && d.Reallocated == nil { d.Reallocated = grown } // SAS drives have no ATA attribute table; their hard-defect signals live in // the SCSI logs. Map them onto the fields the scorer already grades: total // uncorrected read/write/verify errors -> uncorrectable sectors, and the // pending (to-be-reassigned) defect count -> current pending sectors. if d.Uncorrectable == nil { if ec := jObj(j, "scsi_error_counter_log"); ec != nil { sum, any := 0, false for _, op := range []string{"read", "write", "verify"} { if u := jInt(ec, op, "total_uncorrected_errors"); u != nil { any = true sum += *u } } if any { d.Uncorrectable = pInt(sum) } } } if d.Pending == nil { if pd := jInt(j, "scsi_pending_defects", "count"); pd != nil { d.Pending = pd } } // ---- NVMe SMART/Health log ---- if nv := jObj(j, "nvme_smart_health_information_log"); nv != nil { d.NvmeCriticalWarning = jInt(nv, "critical_warning") d.NvmeAvailSpare = jInt(nv, "available_spare") d.NvmeAvailSpareThresh = jInt(nv, "available_spare_threshold") d.NvmeMediaErrors = jInt(nv, "media_errors") if d.PowerOnHours == nil { d.PowerOnHours = jInt(nv, "power_on_hours") } if d.PowerCycleCount == nil { d.PowerCycleCount = jInt(nv, "power_cycles") } if d.TempC == nil { d.TempC = jInt(nv, "temperature") } if pu := jInt(nv, "percentage_used"); pu != nil { d.WearPctConsumed = pu d.WearPctRemaining = pInt(100 - *pu) d.WearSrc = "NVMe/percentage_used" } // Treat NVMe media+data integrity errors like uncorrectable sectors. if d.Uncorrectable == nil && d.NvmeMediaErrors != nil { d.Uncorrectable = d.NvmeMediaErrors } } // Capture smartctl passthrough diagnostics. if msgs, ok := jLeaf(j, "smartctl", "messages").([]interface{}); ok { var parts []string for _, mm := range msgs { if mo, ok := mm.(map[string]interface{}); ok { if s := jStr(mo, "string"); s != "" { parts = append(parts, s) } } } d.SmartctlMessages = strings.Join(parts, "; ") } } // jsonUsable reports whether the parsed object carries real identity + health. func jsonUsable(j map[string]interface{}) bool { if j == nil { return false } hasID := jStr(j, "model_name") != "" || jStr(j, "scsi_model_name") != "" if !hasID { return false } if jObj(j, "ata_smart_attributes") != nil || jObj(j, "smart_status") != nil || jObj(j, "nvme_smart_health_information_log") != nil || jInt(j, "scsi_percentage_used_endurance_indicator") != nil { return true } return false } // first returns the first non-empty string in vals, or "". func first(vals ...string) string { for _, v := range vals { if v != "" { return v } } return "" }