374 lines
9.1 KiB
Go
374 lines
9.1 KiB
Go
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
|
|
}
|