package collector import ( "bee/audit/internal/schema" "encoding/json" "log/slog" "os/exec" "sort" "strconv" "strings" ) type sensorsDoc map[string]map[string]any func collectSensors() *schema.HardwareSensors { doc, err := readSensorsJSONDoc() if err != nil { slog.Info("sensors: unavailable, skipping", "err", err) return nil } sensors := buildSensorsFromDoc(doc) if sensors == nil || (len(sensors.Fans) == 0 && len(sensors.Power) == 0 && len(sensors.Temperatures) == 0 && len(sensors.Other) == 0) { return nil } slog.Info("sensors: collected", "fans", len(sensors.Fans), "power", len(sensors.Power), "temperatures", len(sensors.Temperatures), "other", len(sensors.Other), ) return sensors } func readSensorsJSONDoc() (sensorsDoc, error) { out, err := exec.Command("sensors", "-j").Output() if err != nil { return nil, err } var doc sensorsDoc if err := json.Unmarshal(out, &doc); err != nil { return nil, err } return doc, nil } func buildSensorsFromDoc(doc sensorsDoc) *schema.HardwareSensors { if len(doc) == 0 { return nil } result := &schema.HardwareSensors{} seen := map[string]struct{}{} chips := make([]string, 0, len(doc)) for chip := range doc { chips = append(chips, chip) } sort.Strings(chips) for _, chip := range chips { features := doc[chip] location := sensorLocation(chip) keys := make([]string, 0, len(features)) for key := range features { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { if strings.EqualFold(key, "Adapter") { continue } feature, ok := features[key].(map[string]any) if !ok { continue } name := strings.TrimSpace(key) if name == "" { continue } switch classifySensorFeature(feature) { case "fan": item := buildFanSensor(name, location, feature) if item == nil || duplicateSensor(seen, "fan", item.Name) { continue } result.Fans = append(result.Fans, *item) case "temp": item := buildTempSensor(name, location, feature) if item == nil || duplicateSensor(seen, "temp", item.Name) { continue } result.Temperatures = append(result.Temperatures, *item) case "power": item := buildPowerSensor(name, location, feature) if item == nil || duplicateSensor(seen, "power", item.Name) { continue } result.Power = append(result.Power, *item) default: item := buildOtherSensor(name, location, feature) if item == nil || duplicateSensor(seen, "other", item.Name) { continue } result.Other = append(result.Other, *item) } } } return result } func parseSensorsJSON(raw []byte) (*schema.HardwareSensors, error) { var doc sensorsDoc err := json.Unmarshal(raw, &doc) if err != nil { return nil, err } return buildSensorsFromDoc(doc), nil } func duplicateSensor(seen map[string]struct{}, sensorType, name string) bool { key := sensorType + "\x00" + name if _, ok := seen[key]; ok { return true } seen[key] = struct{}{} return false } func sensorLocation(chip string) *string { chip = strings.TrimSpace(chip) if chip == "" { return nil } return &chip } func classifySensorFeature(feature map[string]any) string { for key := range feature { switch { case strings.Contains(key, "fan") && strings.HasSuffix(key, "_input"): return "fan" case strings.Contains(key, "temp") && strings.HasSuffix(key, "_input"): return "temp" case strings.Contains(key, "power") && (strings.HasSuffix(key, "_input") || strings.HasSuffix(key, "_average")): return "power" case strings.Contains(key, "curr") && strings.HasSuffix(key, "_input"): return "power" case strings.HasPrefix(key, "in") && strings.HasSuffix(key, "_input"): return "power" } } return "other" } func buildFanSensor(name string, location *string, feature map[string]any) *schema.HardwareFanSensor { rpm, ok := firstFeatureInt(feature, "_input") if !ok { return nil } item := &schema.HardwareFanSensor{Name: name, Location: location, RPM: &rpm} if status := sensorStatusFromFeature(feature); status != nil { item.Status = status } return item } func buildTempSensor(name string, location *string, feature map[string]any) *schema.HardwareTemperatureSensor { celsius, ok := firstFeatureFloat(feature, "_input") if !ok { return nil } item := &schema.HardwareTemperatureSensor{Name: name, Location: location, Celsius: &celsius} if warning, ok := firstFeatureFloatWithSuffixes(feature, []string{"_max", "_high"}); ok { item.ThresholdWarningCelsius = &warning } if critical, ok := firstFeatureFloatWithSuffixes(feature, []string{"_crit", "_emergency"}); ok { item.ThresholdCriticalCelsius = &critical } if status := sensorStatusFromFeature(feature); status != nil { item.Status = status } else { item.Status = deriveTemperatureStatus(item.Celsius, item.ThresholdWarningCelsius, item.ThresholdCriticalCelsius) } return item } func buildPowerSensor(name string, location *string, feature map[string]any) *schema.HardwarePowerSensor { item := &schema.HardwarePowerSensor{Name: name, Location: location} if v, ok := firstFeatureFloatWithContains(feature, []string{"power"}); ok { item.PowerW = &v } if v, ok := firstFeatureFloatWithPrefix(feature, "curr"); ok { item.CurrentA = &v } if v, ok := firstFeatureFloatWithPrefix(feature, "in"); ok { item.VoltageV = &v } if item.PowerW == nil && item.CurrentA == nil && item.VoltageV == nil { return nil } if status := sensorStatusFromFeature(feature); status != nil { item.Status = status } return item } func buildOtherSensor(name string, location *string, feature map[string]any) *schema.HardwareOtherSensor { value, unit, ok := firstGenericSensorValue(feature) if !ok { return nil } item := &schema.HardwareOtherSensor{Name: name, Location: location, Value: &value} if unit != "" { item.Unit = &unit } if status := sensorStatusFromFeature(feature); status != nil { item.Status = status } return item } func sensorStatusFromFeature(feature map[string]any) *string { for key, raw := range feature { if !strings.HasSuffix(key, "_alarm") { continue } if number, ok := floatFromAny(raw); ok && number > 0 { status := statusWarning return &status } } return nil } func deriveTemperatureStatus(current, warning, critical *float64) *string { if current == nil { return nil } switch { case critical != nil && *current >= *critical: status := statusCritical return &status case warning != nil && *current >= *warning: status := statusWarning return &status default: status := statusOK return &status } } func firstFeatureInt(feature map[string]any, suffix string) (int, bool) { for key, raw := range feature { if strings.HasSuffix(key, suffix) { if value, ok := floatFromAny(raw); ok { return int(value), true } } } return 0, false } func firstFeatureFloat(feature map[string]any, suffix string) (float64, bool) { return firstFeatureFloatWithSuffixes(feature, []string{suffix}) } func firstFeatureFloatWithSuffixes(feature map[string]any, suffixes []string) (float64, bool) { keys := sortedFeatureKeys(feature) for _, key := range keys { for _, suffix := range suffixes { if strings.HasSuffix(key, suffix) { if value, ok := floatFromAny(feature[key]); ok { return value, true } } } } return 0, false } func firstFeatureFloatWithContains(feature map[string]any, parts []string) (float64, bool) { keys := sortedFeatureKeys(feature) for _, key := range keys { matched := true for _, part := range parts { if !strings.Contains(key, part) { matched = false break } } if matched { if value, ok := floatFromAny(feature[key]); ok { return value, true } } } return 0, false } func firstFeatureFloatWithPrefix(feature map[string]any, prefix string) (float64, bool) { keys := sortedFeatureKeys(feature) for _, key := range keys { if strings.HasPrefix(key, prefix) && strings.HasSuffix(key, "_input") { if value, ok := floatFromAny(feature[key]); ok { return value, true } } } return 0, false } func firstGenericSensorValue(feature map[string]any) (float64, string, bool) { keys := sortedFeatureKeys(feature) for _, key := range keys { if strings.HasSuffix(key, "_alarm") { continue } value, ok := floatFromAny(feature[key]) if !ok { continue } unit := inferSensorUnit(key) return value, unit, true } return 0, "", false } func inferSensorUnit(key string) string { switch { case strings.Contains(key, "humidity"): return "%" case strings.Contains(key, "intrusion"): return "" default: return "" } } func sortedFeatureKeys(feature map[string]any) []string { keys := make([]string, 0, len(feature)) for key := range feature { keys = append(keys, key) } sort.Strings(keys) return keys } func floatFromAny(raw any) (float64, bool) { switch value := raw.(type) { case float64: return value, true case float32: return float64(value), true case int: return float64(value), true case int64: return float64(value), true case json.Number: if f, err := value.Float64(); err == nil { return f, true } case string: if value == "" { return 0, false } if f, err := strconv.ParseFloat(value, 64); err == nil { return f, true } } return 0, false }