Skip to content

Repository files navigation

TrueNAS Fan Controller

A Blazor Server web app that reads temperature and fan speed from a TrueNAS host over SSH and lets you control fan PWM from any browser on your LAN.

This project was made entirely through vibe coding with Claude — no manual code was written. It was created for studying purposes, to explore how far AI-assisted development can go in producing a complete, working application with tests, documentation, and deployment configuration.

Dashboard Screenshot


Quick Start

1. Prerequisites

  • Docker Engine 20.10+ with the Compose v2 plugin (docker compose version should return v2.x)
  • SSH access to your TrueNAS host (root or a user with sysfs read/write access)

2. Create compose.yaml

Create a compose.yaml file with the following content:

services:
  nasfancontroller:
    container_name: nasfancontroller
    image: michele73/truenas-fan-controller:latest
    ports:
      - "8080:8080"
    environment:
      - SSH__Host=192.168.1.100            # IP address of your TrueNAS machine
      - SSH__Port=22
      - SSH__Username=root
      - SSH__Password=changeme             # your SSH password
      - Hardware__TempInputPath=/sys/class/hwmon/hwmon0/temp1_input
      - Hardware__FanInputPath=/sys/class/hwmon/hwmon0/fan1_input
      - Hardware__PwmPath=/sys/class/hwmon/hwmon0/pwm1
      - Hardware__PwmEnablePath=/sys/class/hwmon/hwmon0/pwm1_enable
    restart: unless-stopped
    stop_grace_period: 30s

Update SSH__Host, SSH__Username, and SSH__Password with your TrueNAS SSH credentials.

3. Detect hardware paths

Run the built-in detection utility to discover the correct sysfs paths on your TrueNAS host:

docker compose run --rm nasfancontroller --detect

This connects to TrueNAS over SSH, scans all hwmon chips, and prints the recommended paths. Copy the detected paths back into your compose.yaml environment variables.

4. Run

docker compose up -d

Open the dashboard in your browser: http://<host-ip>:8080

You should see live CPU temperature, CPU usage, fan RPM, and a PWM slider to control fan speed.


Configuration Reference

All configuration is done through environment variables in compose.yaml.

Variable Required Description Default
SSH__Host Yes IP address or hostname of TrueNAS machine
SSH__Port No SSH port 22
SSH__Username Yes SSH user with sysfs access root
SSH__Password Yes SSH password
Hardware__TempInputPath Yes Temperature source (sysfs path or cmd: command)
Hardware__FanInputPath Yes sysfs path to fan RPM input
Hardware__PwmPath Yes sysfs path to PWM control output (0-255)
Hardware__PwmEnablePath No sysfs path to PWM mode control (1=manual, 2=auto)

Temperature source

Hardware__TempInputPath supports two modes:

sysfs path — reads a file that contains millidegrees Celsius (e.g., 45000 = 45.0°C):

Hardware__TempInputPath=/sys/class/thermal/thermal_zone0/temp

cmd: prefix — runs a shell command on the TrueNAS host. Use this when the sysfs temperature doesn't match the TrueNAS dashboard. The command must output millidegrees Celsius:

Hardware__TempInputPath=cmd:midclt call reporting.cpu_temperatures | python3 -c "import sys,json;print(int(json.load(sys.stdin)['cpu']*1000))"

Optional: PwmEnablePath

Hardware__PwmEnablePath is optional. If omitted or the path does not exist, the app logs a warning and continues. Fan speed control via the PWM path still works — only the automatic/manual mode switching is skipped.


Hardware Detection

The --detect flag connects to TrueNAS over SSH and scans for hwmon chips:

docker compose run --rm nasfancontroller --detect

What it does:

  1. Connects using the SSH credentials from the environment variables
  2. Lists all hwmon chips found under /sys/class/hwmon/
  3. For each chip, reads the chip name and available inputs (temp, fan, pwm)
  4. Tests sysfs write access on the recommended chip
  5. Prints a ready-to-paste configuration block

Re-run --detect after a TrueNAS reboot — hwmon numbering can change between reboots.


Running

Start

docker compose up -d

The container pulls the image from Docker Hub on first run. It restarts automatically after host reboots (restart: unless-stopped).

View logs

docker compose logs -f nasfancontroller

Stop

docker compose down

On shutdown, the app sends a final SSH command to restore automatic fan mode on the TrueNAS host. The compose file includes stop_grace_period: 30s to give this enough time to complete.

Update to latest version

docker compose pull
docker compose up -d

Troubleshooting

SSH connection refused

Symptoms: Logs show Connection refused or No route to host

Fix:

  1. Verify the SSH service is running on TrueNAS (check the TrueNAS web UI)
  2. Confirm SSH__Host is the correct IP — prefer IP address over hostname (hostnames may not resolve inside Docker containers)
  3. Confirm SSH__Port matches the TrueNAS SSH port (default 22)
  4. Check that your firewall is not blocking port 22

Test manually: ssh root@<truenas-ip>

SSH authentication failed

Symptoms: Logs show Authentication failed

Fix:

  1. Verify SSH__Username and SSH__Password are correct
  2. Test the credentials manually: ssh root@<truenas-ip>

Wrong hwmon paths

Symptoms: Logs show path not found or no such file or directory for a hardware path

Cause: hwmon chip numbering (hwmon0, hwmon1, ...) is assigned by the kernel at boot and can change after a TrueNAS reboot or kernel/driver update.

Fix: Re-run detection and update your environment variables:

docker compose run --rm nasfancontroller --detect

Temperature doesn't match TrueNAS dashboard

Cause: The sysfs temperature sensor may report a different value than the TrueNAS middleware.

Fix: Use the cmd: prefix to read from the TrueNAS middleware instead:

Hardware__TempInputPath=cmd:midclt call reporting.cpu_temperatures | python3 -c "import sys,json;print(int(json.load(sys.stdin)['cpu']*1000))"

Port conflict

Symptoms: docker compose up fails with address already in use on port 8080

Fix: Change the left side of the port mapping in compose.yaml:

ports:
  - 9090:8080   # change 9090 to any available port

Then access the dashboard at http://<host-ip>:9090.

Container restarts in a loop

Fix:

  1. Check logs: docker compose logs nasfancontroller
  2. The startup validator prints specific error messages for each failure mode
  3. Common causes: wrong SSH credentials, SSH service not running, incorrect hardware paths
  4. Fix the issue, then restart: docker compose up -d

How It Works

The app is a Blazor Server application running in a Docker container on any machine on your LAN. It connects to your TrueNAS host over SSH with password authentication, then reads temperature and fan speed from the TrueNAS sysfs interface every 3 seconds. Readings are pushed to the browser in real time via SignalR (the standard Blazor Server transport). When you move the fan speed slider, the app writes a PWM value to the TrueNAS sysfs fan control path over SSH. On shutdown (SIGTERM), it sends a final SSH command to restore automatic fan mode so the fans continue operating normally if the controller stops running.


Changelog

v1.2 — Startup Reliability & Coverage

  • Fixed the startup crash-loop on power-on — the app now switches the fan to manual mode (pwm_enable=1) before probing PWM write access, so it no longer crashes with a device or resource busy (EBUSY) error when the TrueNAS host boots with the fan under automatic control. This most often appeared after powering the QNAP off and on.
  • Non-fatal hardware validation — a failed hardware probe at startup is now reported in the dashboard instead of crashing the container into a Docker restart loop, with an actionable message when a PWM write is rejected because the fan is in automatic mode.
  • Survives host power cycles — when the TrueNAS host is powered off and back on, the app automatically reconnects, re-asserts manual fan mode, and re-validates hardware paths without needing a container restart. Half-open SSH connections are now detected via a per-command timeout.
  • Sampling-degraded banner — repeated hardware read failures while connected now surface a visible dashboard warning instead of failing silently.
  • Version shown in the title — the dashboard header and browser tab now display the running version, e.g. "NAS Fan Controller (v1.2)".
  • Test coverage raised to 92% — 183 unit and bUnit tests with an enforced ≥85% line-coverage gate (scripts/coverage.ps1). Restored the SSH retry-loop and reconnect-idempotency tests, removed dead backoff code, and migrated the test stack to xUnit v3.

v1.1 — Resilience & Test Coverage

  • SSH connection resilience — the app now retries automatically when the TrueNAS host is unreachable at startup or disconnects mid-run, on a 60-120s cycle. Authentication failures (wrong credentials) are detected and stop retrying immediately with a clear error.
  • Service resilience — monitoring and fan control skip gracefully while SSH is disconnected (no log spam) and resume automatically when the connection is restored, without requiring a container restart.
  • Dashboard reconnect banner — a visible banner appears in the UI when the SSH connection is lost, showing the reconnecting state. It disappears automatically when the connection is restored.
  • Improved test coverage — 150 unit and bUnit tests covering resilience logic, service behavior, and dashboard state transitions. Test packages updated to bUnit 2.7.2, coverlet 8.0.1, Microsoft.NET.Test.Sdk 18.4.0.

v1.0 — Initial Release

  • Real-time CPU temperature, CPU usage, and fan RPM monitoring
  • Manual fan speed control via PWM slider with debounced auto-apply
  • Rolling 5-minute sparkline charts
  • Hardware path auto-detection utility (--detect)
  • Configurable hardware paths via environment variables
  • Automatic fan mode restoration on shutdown
  • SSH-based remote hardware access
  • Dark-themed responsive dashboard
  • Docker Hub deployment (michele73/truenas-fan-controller)

Development

To build and run from source (requires .NET 10 SDK):

git clone https://github.com/mdima/Truenas-Fan-Controller.git
cd Truenas-Fan-Controller
cp .env.example src/NasFanController/.env
# Edit src/NasFanController/.env with your settings
dotnet run --project src/NasFanController

Run the unit test suite:

dotnet test

Check coverage against the ≥85% line-coverage gate (collects coverage via coverlet.runsettings and fails if below threshold):

pwsh scripts/coverage.ps1

To build the Docker image locally:

docker build -t nasfancontroller -f src/NasFanController/Dockerfile .

About

An AI generated utility to control my TrueNas fan.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages