Skip to content

feat: fan RPM monitoring + FanSpeed alerts#2032

Open
maxmaxme wants to merge 4 commits into
henrygd:mainfrom
maxmaxme:feat/fan-rpm
Open

feat: fan RPM monitoring + FanSpeed alerts#2032
maxmaxme wants to merge 4 commits into
henrygd:mainfrom
maxmaxme:feat/fan-rpm

Conversation

@maxmaxme
Copy link
Copy Markdown

@maxmaxme maxmaxme commented May 21, 2026

📃 Description

Adds fan RPM monitoring as a peer to the existing temperature collection, addressing #1918. Four commits, each layer parallel to Temperature:

  1. agent — walks /sys/class/hwmon/*/fan*_input, skips 0 RPM (unpopulated headers), populates a new Stats.Fans map. Linux-only via a build-tagged hwmonRoot const; other platforms get an empty string and skip collection entirely. gopsutil has no equivalent for fans, hence the small custom sysfs walker per the issue's maintainer guidance.
  2. hub UI — new Fans chart on the system detail page, RPM-formatted, with $fanFilter filter bar. Reads stats.f and renders one line per sensor, mirroring TemperatureChart.
  3. alerts — new FanSpeed alert type. Fires when the highest fan RPM across all sensors goes above the user's threshold. Same per-sensor mapSums aggregation as Temperature so multi-minute alerts name the loudest fan in the notification descriptor (e.g. "Highest fan pwmfan_fan1").
  4. docs — readme updated to list fan speed under Supported metrics and in the alerts feature line. Companion PR opened against beszel-docs for the same change on the website (see below).

Verified end-to-end on a Raspberry Pi 5 (Active Cooler): agent reading pwmfan_fan1 = 3782 RPM, hub persisting "f":{"pwmfan_fan1":3766} next to "t":{...} in system_stats.stats, UI rendering the chart live, alert tests passing in both one-minute and multi-minute paths.

Notes for the reviewer

  • The new FanSpeed option is added via a sequential migration (1_add_fanspeed_alert.go) — applies on existing installs, idempotent, with a working down-migration. Verified on a real DB snapshot pulled from a live Pi install: apply → re-apply → revert, all clean.
  • Locale .po updates intentionally omitted to match the upstream "update translations" cadence — until then the new msgids (Fans, Fan speeds of system sensors) fall back to English in every locale, same as any other newly-introduced string.
  • Info.DashboardFan (max RPM across sensors) exists solely to give the alert path a single number to compare against the threshold. The chart keeps reading the full Stats.Fans map so every sensor is drawn separately — no aggregation in the visualisation.

📖 Documentation

Companion PR: henrygd/beszel-docs#61 — two-line update to en/guide/what-is-beszel.md (Supported metrics + Alerts feature line).

🪵 Changelog

➕ Added

  • Fan RPM collection from /sys/class/hwmon/*/fan*_input on Linux hosts.
  • Stats.Fans wire field (CBOR 36,keyasint,omitempty, JSON f,omitempty). Omitted entirely when no fan reports a non-zero RPM.
  • Info.DashboardFan — max RPM across sensors, used by the new alert.
  • FanChart on the system detail page mirroring TemperatureChart (RPM units, $fanFilter filter bar, sortable legend).
  • FanSpeed alert type with multi-minute aggregation and per-sensor descriptors.
  • Sequential migration 1_add_fanspeed_alert.go extending the alerts collection's name-select with FanSpeed (idempotent, with down-migration).
  • Unit tests for the hwmon walker (happy path, zero-RPM skip, labeled vs. unlabeled, missing root, empty root) and for the one-minute / multi-minute FanSpeed alert paths.

✏️ Changed

  • agent/system.go calls updateFans() immediately after updateTemperatures().
  • readme.md — fan speed added to the supported-metrics list and alerts feature line (mirrored in companion docs PR).

📷 Screenshots

The new Fans card sits under Temperature on the system detail page:
Screenshot 2026-05-22 at 00 56 55

maxmaxme added 4 commits May 22, 2026 00:21
Mirrors temperature collection: walks /sys/class/hwmon/*, reads every
fan*_input it finds, keys entries by "<chip>_<label-or-fan-idx>", skips
0 RPM (unpopulated headers). Linux-only via hwmonRoot const that's
empty on other platforms.

New field Stats.Fans (CBOR tag 36, JSON "f") — wire-compatible with
older hubs thanks to omitempty / CBOR's forward compatibility.

Addresses henrygd#1918.
Mirrors TemperatureChart for the new Stats.Fans field:
- $fanFilter store + types update (stats.f)
- FanChart component beneath Temperature, RPM units
- Wires into both the legacy single-tab grid and the new tabbed view

Verified end-to-end on a Raspberry Pi 5 with pwmfan_fan1 graphing
~3760-3770 RPM live alongside cpu_thermal/rp1_adc temperature.

Locale .po updates intentionally omitted — they belong in a separate
"update translations" commit per the upstream pattern.
Triggers when the highest fan RPM across all collected sensors goes
above a user-set threshold. Mirrors the Temperature alert path at
every layer:

- system.Info.DashboardFan (CBOR tag 24, JSON "df") — max RPM across
  Stats.Fans, computed in the agent each tick. Used ONLY by the alert
  for its single-threshold compare; the FanChart in the UI keeps
  reading the full Stats.Fans map and draws every sensor separately.
- alerts_system.go: new "FanSpeed" case in both the one-minute fast
  path and the multi-minute aggregation. Uses the same per-sensor
  mapSums pattern as Temperature so the descriptor names the loudest
  fan (e.g. "Highest fan pwmfan_fan1") in notifications.
- SystemAlertStats gains a Fans map[string]float32 mirror of Temperatures.
- New sequential migration 1_add_fanspeed_alert.go extends the existing
  alerts.name select with "FanSpeed" (idempotent, with a down-migration
  that strips it). Slots FanSpeed next to Temperature so the dropdown
  order matches the chart ordering on the system page.
- Tests: one-minute + multi-minute FanSpeed alert cases added beside
  the existing Temperature ones.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant