Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

homeserver-monitor

License: MIT

Lightweight Linux host monitoring over MQTT: any number of machines (servers, NAS boxes, hypervisors, Raspberry Pis — anything with /sys/class/hwmon) publish CPU/RAM/temperature/disk metrics, which flow into Home Assistant (auto-discovery) and optionally onto a dedicated ESP32 HUD-style display — which keeps working whether or not Home Assistant is up.

Features

  • sysmon2mqtt.py — a single Python file (stdlib + psutil + paho-mqtt), run via systemd on every monitored host. Collects CPU/RAM/ load/uptime, temperatures from /sys/class/hwmon (CPU, motherboard, disks), and filesystem usage; publishes a flat JSON payload to MQTT and generates Home Assistant MQTT discovery configs. Works with paho-mqtt 1.x and 2.x.
  • ESP32-S3 + ST7789 dashboard (esphome/, optional) — HUD-style UI with glowing corner accents, dynamic host discovery (wildcard subscription to sysmon/+/state, no YAML changes needed when you add another machine), a CPU history sparkline, per-disk temperatures, an animated boot/connecting screen, backlight dimming, and a button for manual page switching. The display reads MQTT directly — it doesn't depend on Home Assistant.
  • 14 built-in color themes (esphome/theme.yaml) — from a muted Nord to a neon Synthwave to popular editor palettes (Tokyo Night, Catppuccin, Gruvbox, One Dark) — plus a two-layer color model: every metric (CPU/RAM/ disk/temperature/WiFi/uptime) has its own identity color, and crossing a warning/critical threshold always overrides to a universal yellow/red.

How it works

┌──────────────┐  hwmon/psutil   ┌──────────────┐
│ Linux host #1│───────────────▶│ sysmon2mqtt  │──┐
└──────────────┘                 │ (systemd)    │  │ MQTT publish
┌──────────────┐                 └──────────────┘  │ every N seconds
│ Linux host #2│───────────────▶ (another copy) ──┤
└──────────────┘        ...                        ▼
                                          ┌──────────────┐
                                          │  MQTT broker │
                                          └──┬────────┬──┘
                              HA MQTT discovery│      │wildcard subscribe
                                             ▼        ▼
                                   ┌──────────────┐ ┌──────────────┐
                                   │ Home         │ │ ESP32-S3 +   │
                                   │ Assistant    │ │ ST7789 HUD   │
                                   │ (optional)   │ │ (optional)   │
                                   └──────────────┘ └──────────────┘

The daemon runs on the physical host (not in an LXC/VM), because containers don't have access to /sys/class/hwmon.

Requirements

Role Requirement
Host(s) any Linux with systemd and /sys/class/hwmon (e.g. Debian, Ubuntu, Proxmox VE, OpenMediaVault)
MQTT broker any (e.g. Mosquitto), reachable from the hosts and (optionally) the ESP32
Display optional: ESP32-S3 + ST7789 3.2" 240×320, 4-wire SPI

The display isn't required — the daemon works standalone with any MQTT client or Home Assistant.

Installation: host

apt install -y python3-psutil python3-paho-mqtt lm-sensors
modprobe drivetemp && echo drivetemp >> /etc/modules
sensors-detect --auto        # saves modules to /etc/modules-load.d/
sensors                      # sanity check: CPU/disk temps should show up

mkdir -p /opt/sysmon2mqtt
cp host/sysmon2mqtt.py /opt/sysmon2mqtt/
cp host/sysmon2mqtt.service /etc/systemd/system/
# edit Environment= in the unit: broker, auth, SYSMON_MOUNTS
systemctl daemon-reload && systemctl enable --now sysmon2mqtt
journalctl -u sysmon2mqtt -f   # should show "HA discovery: published N entities"

Repeat on every host you want to monitor — new hosts show up on the dashboard automatically, with no ESPHome config changes.

End-to-end check (from any machine): mosquitto_sub -h <broker> -t 'sysmon/#' -v

Configuration (environment variables)

The daemon is configured entirely through Environment= in the systemd unit — no config files:

Variable Default Description
SYSMON_MQTT_HOST 192.168.1.10 broker address
SYSMON_MQTT_PORT 1883 port
SYSMON_MQTT_USER / SYSMON_MQTT_PASS empty auth (empty = anonymous)
SYSMON_INTERVAL 15 seconds between publishes
SYSMON_HOSTNAME socket.gethostname() override the name used in topics
SYSMON_HA_PREFIX homeassistant discovery prefix
SYSMON_MOUNTS / comma-separated mount points, e.g. /,/srv/data

DynamicUser=yes + ProtectSystem=strict in the unit — the daemon only reads sysfs/procfs and writes nothing, so the full systemd sandbox works with no extra privileges.

MQTT contract

Topics

Topic Retained Payload Published by
sysmon/<hostname>/state no JSON (see below) daemon, every SYSMON_INTERVAL s
sysmon/<hostname>/status yes online / offline daemon; offline is also the LWT
homeassistant/sensor/sysmon_<hostname>/<key>/config yes JSON discovery daemon, once at startup

state payload (flat JSON)

Key Type Unit Source
cpu_percent float % psutil.cpu_percent()
load_1, load_5, load_15 float os.getloadavg()
ram_percent float % psutil.virtual_memory()
ram_used_gb, ram_total_gb float GB same
uptime_days float days psutil.boot_time()
cpu_temp float|null °C heuristic, see below
disk_<mount>_percent float % psutil.disk_usage() for each of SYSMON_MOUNTS
disk_<mount>_free_gb float GB same
temp_<chip>[_<model>_<sdX>]_<label> float °C hwmon, see below

Example:

{
  "cpu_percent": 12.5, "load_1": 0.42, "load_5": 0.38, "load_15": 0.31,
  "ram_percent": 47.0, "ram_used_gb": 3.61, "ram_total_gb": 7.68,
  "uptime_days": 12.4, "cpu_temp": 54.3,
  "disk_root_percent": 31.0, "disk_root_free_gb": 18.2,
  "temp_k10temp_tctl": 54.3,
  "temp_drivetemp_wdc_wd20efrx_sda_temp1": 38.0,
  "temp_drivetemp_wdc_wd20efrx_sdb_temp1": 39.0
}

Evolution rules: keys can be added; renaming or removing an existing key is a breaking change (it leaves dead entities in HA discovery, and ESPHome stops parsing it). Hardware-derived keys (temp_*, disk_*) are inherently dynamic.

Home Assistant discovery is published once, after the first metrics collection (retain=true). Each metric is a separate sensor entity with value_template: {{ value_json.<key> }}, a shared availability_topic, and a shared device (identifiers: [sysmon_<hostname>]). Unit mapping: *_percent→%, *_gb→GB (device_class: data_size), cpu_temp/temp_*→°C (device_class: temperature), everything state_class: measurement.

Sensor reading — implementation details

hwmon (read_hwmon_temps): iterates /sys/class/hwmon/hwmon*, reads the chip name from <hwmon>/name (e.g. k10temp, coretemp, nct6775, drivetemp, nvme). For drivetemp/nvme, it appends the disk model (device/model) and block device name (sda, nvme0n1) to the key, to tell apart multiple identical disks in an array. Readings ≤0 °C and >150 °C are filtered out (garbage that some Super I/O chips return). The code doesn't rely on hwmonN numbers (they shift across kernel updates), only on the chip's name.

cpu_temp heuristic: first match from a list of prefixes — k10temp_tctl, k10temp_tdie, coretemp_package, coretemp_temp1. Note: Tctl on some AMD CPUs is offset by a fixed amount from the real temperature (Tdie) — if it looks suspiciously high or low, check both keys in the raw payload.

Lifecycle: psutil.cpu_percent(interval=None) is called once at startup (calibration; subsequent calls return the average since the previous one — interval=1 is intentionally not used, since it would block the loop). SIGTERM/SIGINT trigger a clean shutdown (publish offline, loop_stop, disconnect). Discovery is published only after the first metrics collection, so the full set of dynamic keys is known.

ESPHome display — details

  • ili9xxx platform, ST7789V model, native 240×320, rotation: 90 for landscape 320×240.
  • Data: wildcard subscription to sysmon/+/state (registered in on_boot, callback labrax::store_payload in esphome/display_draw.h). Hosts are discovered dynamically — name taken from the topic, payload parsed with ArduinoJson into the labrax::hosts() registry. The HA API is deliberately not used — the display should keep working even when HA is down.
  • The entire drawing layer (HUD styling, color thresholds, CPU history sparkline, per-host pages) lives in esphome/display_draw.h; the YAML only holds fonts, the page schedule, and the call to labrax::draw_page.
  • Color themes: the palette lives in esphome/theme.yaml (included via packages:) — 15 th_* substitutions injected into labrax::set_theme() in on_boot: 9 UI colors (background/text/frames/thresholds) + 6 per-metric identity colors (th_cpu/th_ram/th_disk/th_temp/th_wifi/th_time — the color of the icon and of the bar/sparkline fill while the metric is OK; crossing a warn/crit threshold still overrides to the universal warning/ critical color). Changing the theme means swapping the hex values in the file (ready-made palettes in the comments: HUD, Amber CRT, Green Phosphor, Synthwave, Dracula, Nord, Solarized Dark, Ice, Tokyo Night, Catppuccin Mocha, One Dark, Obsidian, Gruvbox Dark, Cyberpunk) and reflashing.
  • Backlight and button: PWM (ledc 2 kHz) + light.monochromatic; full brightness for awake_secs (a substitution), then dims to dim_brightness. On a dimmed screen the button just wakes it (current page stays); on an awake screen it advances the page and pauses auto-rotation for 30 s; a double-click on an awake screen toggles auto-rotation on/off (state doesn't survive a reboot). Page rotation: the main screen shows for 24 s, the other pages (one combined details+disks page per host, plus a status page) for 6 s each; the number of pages is dynamic, based on how many hosts have been discovered. A host's combined page shows a compact LOAD/UPTIME/RAM/DYSK summary strip plus a list of every reported disk's temperature below it when it has at least one disk sensor; with none, it falls back to a fuller 2×2 tile layout with more detail (5m/15m load average, RAM in GB, free disk space).
  • Default color thresholds: CPU temp >75 °C red, >60 °C yellow; disks

    50/45 °C; CPU/RAM/disk % >85/70. Host data older than 60 s shows "NO DATA".

Wiring the display

esphome/labrax-display.yaml is written for one specific, cheaply available combo: an ESP32-S3 "Super Mini" (PCB marking HW-747) + a ST7789V 3.2" 240×320, 4-wire SPI panel (e.g. a GoldenMorning T320H8-C40-11 module). The ESP32-S3 has a GPIO matrix, so SPI works on any free pins — with a different board or display, just swap the GPIO numbers in the YAML.

Hardware note: "HW-747" boards are often listed as 16 MB flash, but are physically 4 MB (S3FH4R2 variant, 2 MB PSRAM in quad mode, not octal). flash_size: 16MB in the config causes a bootloop (Failed to verify partition table) — hence flash_size: 4MB in the YAML.

Wiring

Display pin Signal ESP32-S3 GPIO
GND ground GND
VCC power (3–5V) 3V3
SCL SPI clock GP4
SDA SPI MOSI GP5
RES reset GP8
DC command/data GP7
CS chip select GP6
BL backlight GP9 (PWM)

Optional page button: a momentary switch between GP10 and GND (internal pull-up in the config, no extra components needed) — everything works without it, the screen just dims after the timeout and pages can't be switched manually.

Pins GP3/GP45/GP46 (strapping — boot mode selection) and GP19/GP20 (native USB D+/D−) are deliberately left out of the wiring.

First flash

pipx install esphome                                   # install the CLI
cp esphome/secrets.yaml.example esphome/secrets.yaml   # fill in wifi_*, mqtt_*
esphome run esphome/labrax-display.yaml

Boards with native USB (rather than a UART bridge, like most ESP32-S3s) sometimes need the bootloader forced manually on the first flash: hold BOOT, press and release RST (while still holding BOOT), release BOOT after ~1–2 s. If the port isn't auto-detected, check ls /dev/ttyACM* (ESP32-S3 with native USB shows up as ttyACM*, not ttyUSB*).

After the first boot, worth checking: colors (invert_colors in the YAML — flip it if they look inverted), orientation (rotation: 90), and whether the display is actually receiving data (sysmon2mqtt must be running and publishing to sysmon/<hostname>/state — until there's at least one host, the screen shows a waiting message, which is expected).

Common issues

  • AttributeError: module 'paho.mqtt.client' has no attribute 'CallbackAPIVersion' — older distros ship paho-mqtt 1.x in their repos (no CallbackAPIVersion, added in 2.0), newer ones ship 2.x (where that parameter is required). sysmon2mqtt.py detects the version via hasattr() and handles both automatically — this shouldn't need any manual fix, but if you still see this error, check you're actually running the current version of the file.
  • No motherboard temperatures — try modprobe nct6775; if the Super I/O chip is an ITE part, modprobe it87 (sometimes needs acpi_enforce_resources=lax on the kernel cmdline — risk of ACPI conflicts, check dmesg first).
  • drivetemp doesn't show any disks behind a SATA controller (common with M.2-to-SATA cards) — the controller isn't passing through SCT/SMART commands. Diagnose with smartctl -x /dev/sda | grep -i sct. A smartctl --json fallback is on the roadmap; there's no built-in workaround yet.
  • hwmonN numbering changes across kernel updates — the code deliberately doesn't rely on the numbers, only on the name field in /sys/class/hwmon/hwmonN/name, so this shouldn't require any configuration changes.

Repo layout

homeserver-monitor/
├── host/
│   ├── sysmon2mqtt.py          # the daemon (a single file, deliberately)
│   └── sysmon2mqtt.service     # systemd unit
└── esphome/
    ├── labrax-display.yaml     # ESP32-S3 + ST7789 config
    ├── display_draw.h          # dashboard drawing layer
    ├── theme.yaml              # 14 built-in color themes
    └── secrets.yaml.example    # secrets template (WiFi/MQTT)

Project status

Daemon and display tested end-to-end on real hardware, under active development. On the roadmap: a smartctl fallback for disks behind controllers without SCT/SMART passthrough, pytest tests with hwmon fixtures, and case fan control through the same ESP32.

License

MIT — do what you want with it, just keep the copyright notice.

About

Lightweight Linux host monitoring over MQTT with Home Assistant auto-discovery and an optional ESP32 HUD dashboard

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages