mirror of
https://github.com/henrygd/beszel.git
synced 2026-08-16 07:17:46 +02:00
Adds fan RPM monitoring as a peer to the existing temperature collection, addressing #1918. --------- Co-authored-by: henrygd <hank@henrygd.me>
102 lines
2.8 KiB
Go
102 lines
2.8 KiB
Go
package agent
|
|
|
|
import (
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/henrygd/beszel/agent/utils"
|
|
"github.com/henrygd/beszel/internal/entities/system"
|
|
)
|
|
|
|
type fanSensor struct {
|
|
key, path string
|
|
}
|
|
|
|
var getFanSensors = newFanSensorCache(hwmonRoot)
|
|
|
|
func newFanSensorCache(root string) func() ([]fanSensor, error) {
|
|
return sync.OnceValues(func() ([]fanSensor, error) {
|
|
return discoverHwmonFans(root)
|
|
})
|
|
}
|
|
|
|
// updateFans populates systemStats.Fans from the host's hwmon sysfs tree.
|
|
// No-op on platforms where hwmon isn't available (see fans_other.go).
|
|
func (a *Agent) updateFans(systemStats *system.Stats) {
|
|
if hwmonRoot == "" {
|
|
return
|
|
}
|
|
sensors, err := getFanSensors()
|
|
if err != nil {
|
|
slog.Debug("Error reading fans", "err", err)
|
|
return
|
|
}
|
|
fans := readFanSensors(sensors)
|
|
if len(fans) == 0 {
|
|
return
|
|
}
|
|
systemStats.Fans = fans
|
|
// Note: Commented out because we don't currently use this value in the UI.
|
|
// Compute the single "dashboard" value used by the FanSpeed alert.
|
|
// Per-sensor RPMs live in Stats.Fans and drive the multi-line FanChart
|
|
// in the UI; the alert path only needs one number to compare against
|
|
// the user's threshold, so we use the highest RPM across all fans
|
|
// a.systemInfo.DashboardFan = 0
|
|
// for _, rpm := range fans {
|
|
// if rpm > a.systemInfo.DashboardFan {
|
|
// a.systemInfo.DashboardFan = rpm
|
|
// }
|
|
// }
|
|
}
|
|
|
|
// readHwmonFans walks the given hwmon root (typically /sys/class/hwmon) and
|
|
// returns a map of "<chip>_<label-or-fan-idx>" → RPM for every fan*_input
|
|
// file it finds. Zero RPM is retained because it can represent a real fan that
|
|
// has stopped; negative and malformed readings are ignored.
|
|
func readHwmonFans(root string) (map[string]uint16, error) {
|
|
sensors, err := discoverHwmonFans(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return readFanSensors(sensors), nil
|
|
}
|
|
|
|
func discoverHwmonFans(root string) ([]fanSensor, error) {
|
|
entries, err := os.ReadDir(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var sensors []fanSensor
|
|
for _, entry := range entries {
|
|
chipDir := filepath.Join(root, entry.Name())
|
|
chipName := utils.ReadStringFile(filepath.Join(chipDir, "name"))
|
|
if chipName == "" {
|
|
chipName = entry.Name()
|
|
}
|
|
inputs, _ := filepath.Glob(filepath.Join(chipDir, "fan*_input"))
|
|
for _, inputPath := range inputs {
|
|
base := strings.TrimSuffix(filepath.Base(inputPath), "_input")
|
|
label := utils.ReadStringFile(filepath.Join(chipDir, base+"_label"))
|
|
key := chipName + "_" + base
|
|
if label != "" {
|
|
key = chipName + "_" + label
|
|
}
|
|
sensors = append(sensors, fanSensor{key, inputPath})
|
|
}
|
|
}
|
|
return sensors, nil
|
|
}
|
|
|
|
func readFanSensors(sensors []fanSensor) map[string]uint16 {
|
|
fans := make(map[string]uint16, len(sensors))
|
|
for _, sensor := range sensors {
|
|
if rpm, ok := utils.ReadUintFile(sensor.path); ok {
|
|
fans[sensor.key] = uint16(rpm)
|
|
}
|
|
}
|
|
return fans
|
|
}
|