A Cockpit plugin for Ubuntu Server that visualises hardware temperature data
(CPU, NVMe, etc.) as interactive line charts with configurable thresholds.
Data is collected and archived by PCP (Performance Co-Pilot) via the
pmdalmsensors PMDA, giving you up to 120 days of history with automatic
archive rotation and disk-budget enforcement.
This is a personal hobby project I build for my own use and publish in case it's useful to someone else. I work on it in my spare time, so issues and PRs are welcome but replies may be slow. Use at your own risk.
- Historical charts — query PCP archives up to 1 year back
- Sensor grouping — configurable groups (CPU, NVMe, etc.) with custom labels
- Threshold lines — global defaults (70 / 75 / 80 °C) plus per-sensor overrides
- Date range controls — quick presets (1h, 24h, 7d, 30d, 90d, 1y) and custom range
- Auto downsampling — step size scales with time range (1 min → 6 h)
- Metric discovery — UI button lists all available PCP lmsensors metrics
- Archive rotation — pmlogger_daily + cron-based disk budget
- Zero external deps — plain JS, inline SVG chart, no build step
cockpit-temps/
├── manifest.json # Cockpit plugin manifest
├── index.html # Main HTML page
├── app.js # Application logic + SVG chart
├── style.css # Styles
└── config/
└── sensors.json # Sensor mapping, thresholds, retention config ← you must edit this
scripts/
├── setup_pcp_temps.sh # Install PCP + lmsensors PMDA + rotation
├── install_plugin.sh # Deploy plugin into Cockpit
└── uninstall_plugin.sh # Remove plugin (optional --purge)
git clone <this-repo> cockpit-temps-repo
cd cockpit-temps-reposudo bash scripts/setup_pcp_temps.shThis will:
- Install
lm-sensors,pcp,cockpit,cockpit-pcp - Run
sensors-detectto load kernel sensor modules - Install the lmsensors PMDA
- Configure
pmloggerto archive lmsensors metrics every 60 s - Set up archive rotation (120 days retention, 10 GB max)
- Enable and start all services
Configurable variables (set before running or edit the script):
| Variable | Default | Description |
|---|---|---|
RETENTION_DAYS |
120 | Keep archives this many days |
MAX_SIZE_GB |
10 | Max total archive disk usage |
PMLOGGER_INTERVAL |
60 | Logging interval in seconds |
Example:
sudo RETENTION_DAYS=180 MAX_SIZE_GB=2 bash scripts/setup_pcp_temps.shThis step is required on every machine. The default
sensors.jsonis configured for specific hardware (an Intel CPU and one NVMe drive at a specific PCI address). Your system will almost certainly have different metric names. The plugin will load but show no data until this is done.
Find your metric names:
pminfo -t lmsensorsExample output (your output will differ):
lmsensors.coretemp_isa_0000.package_id_0 [coretemp-isa-0000 Package id 0]
lmsensors.coretemp_isa_0000.core_0 [coretemp-isa-0000 Core 0]
lmsensors.coretemp_isa_0000.core_1 [coretemp-isa-0000 Core 1]
lmsensors.nvme_pci_0100.composite [nvme-pci-0100 Composite]
lmsensors.nvme_pci_0100.sensor_1 [nvme-pci-0100 Sensor 1]
The naming convention is lmsensors.<chip>.<feature> where the chip name is
the adapter name from sensors with dashes replaced by underscores
(e.g. coretemp-isa-0000 → coretemp_isa_0000).
Common hardware differences:
| Hardware | Example chip name |
|---|---|
| Intel CPU | coretemp_isa_0000 |
| AMD CPU | k10temp_pci_00c3 (address varies) |
| NVMe at 01:00 | nvme_pci_0100 |
| NVMe at 02:00 | nvme_pci_0200 |
| Second NVMe | nvme_pci_0300 (address varies) |
Edit cockpit-temps/config/sensors.json to match your metric names. A
minimal example for one CPU package sensor and one NVMe:
{
"groups": [
{
"id": "cpu",
"label": "CPU",
"sensors": [
{
"id": "cpu_package",
"label": "CPU Package",
"metric": "lmsensors.coretemp_isa_0000.package_id_0",
"default": true,
"thresholds": null
}
]
},
{
"id": "nvme",
"label": "NVMe",
"sensors": [
{
"id": "nvme_composite",
"label": "NVMe Composite",
"metric": "lmsensors.nvme_pci_0100.composite",
"default": true,
"thresholds": [
{ "value": 75, "label": "NVMe Composite Warning", "color": "#FFA726" },
{ "value": 80, "label": "NVMe Composite Critical", "color": "#E53935" }
]
}
]
}
],
"thresholds": {
"global": [
{ "value": 70, "label": "Warning 70\u00b0C", "color": "#FFA726", "style": "dashed" },
{ "value": 75, "label": "Smartd 75\u00b0C", "color": "#FF7043", "style": "dashed" },
{ "value": 80, "label": "Critical 80\u00b0C", "color": "#E53935", "style": "dashed" }
]
},
"retention": {
"days": 120,
"maxSizeGB": 10
},
"archiveBase": "/var/log/pcp/pmlogger"
}Remove sensor entries whose metrics do not exist on your hardware — they will simply produce no data but cause no errors.
sudo bash scripts/install_plugin.shThis also disables PCP archive compression ($PCP_COMPRESSAFTER=never), which
is required because pmrep cannot read .xz-compressed archives when given a
directory argument. See PCP Archive Compression
for details.
Re-run this command any time you change sensors.json.
Navigate to https://<server-ip>:9090 and click Temperatures in the menu.
Defined under thresholds.global in sensors.json. These horizontal lines are
drawn on every chart regardless of which sensors are selected.
Each sensor entry can include a thresholds array to add sensor-specific lines:
{
"id": "nvme_composite",
"label": "NVMe Composite",
"metric": "lmsensors.nvme_pci_0100.composite",
"thresholds": [
{ "value": 75, "label": "NVMe Composite Warning", "color": "#FFA726" },
{ "value": 80, "label": "NVMe Composite Critical", "color": "#E53935" }
]
},
{
"id": "nvme_sensor1",
"label": "NVMe Sensor 1 (controller chip)",
"metric": "lmsensors.nvme_pci_0100.sensor_1",
"thresholds": [
{ "value": 82, "label": "NVMe Sensor 1 Warning", "color": "#FFA726" },
{ "value": 90, "label": "NVMe Sensor 1 Critical", "color": "#E53935" }
]
}Set "thresholds": null to inherit only the global thresholds.
NVMe drives expose multiple temperature sensors that measure different physical locations and have very different normal operating ranges:
| Sensor | What it measures | Normal range under load |
|---|---|---|
| Composite | Drive-level aggregate | 50–70 °C |
| Sensor 1 | Controller chip | 65–82 °C |
| Sensor 2 | NAND flash | 50–70 °C |
Samsung PM9A1 (MZVL8512HELU, OEM 980 Pro Gen4): The controller chip
(Sensor 1) routinely runs 15–20 °C hotter than the composite temperature. This
is expected behaviour — the drive begins thermal throttling only when the
composite temperature approaches 82 °C. The authoritative check for thermal
problems is nvme smart-log:
nvme smart-log /dev/nvme0 | grep -E "Warning Temperature Time|Critical Composite Temperature Time|Thermal Management T[12] Trans Count"If all three fields report 0, the drive has never throttled and the higher
Sensor 1 readings are normal. A global threshold calibrated against composite
temperatures (e.g. 75 °C) will fire false alarms for Sensor 1. Use
sensor-specific thresholds as shown above to avoid this.
Sensors with "default": true are pre-checked when the plugin loads. All
others must be selected manually.
PCP archive compression must be disabled for this plugin to work.
pmlogger_daily compresses rotated PCP archives by default (.0 → .0.xz,
.meta → .meta.xz) but leaves .index files uncompressed. When pmrep is
given a directory as its -a argument, it tries to open all archive sets found
there. The orphaned .index files (pointing to compressed .0.xz/.meta.xz
that pmrep cannot read) cause pmrep to fail with
PM_ERR_NAME Unknown metric name for every metric.
The install script handles this automatically by setting
$PCP_COMPRESSAFTER=never in /etc/pcp/pmlogger/control.d/local.
If you installed PCP before running the install script, some archives may already be compressed. To fix this:
-
Move
.xzfiles out of the archive directory:ARCHIVE_DIR=/var/log/pcp/pmlogger/$(hostname) mkdir -p /tmp/pcp-compressed-backup mv "$ARCHIVE_DIR"/*.xz /tmp/pcp-compressed-backup/ 2>/dev/null
-
Remove orphaned
.indexfiles (those whose matching.0file is missing):for idx in "$ARCHIVE_DIR"/*.index; do base="${idx%.index}" if [[ ! -f "$base.0" ]]; then rm -v "$idx" fi done
-
Restart pmlogger:
sudo systemctl restart pmlogger
-
pmlogger_daily runs via systemd timer (or cron) once per day:
- Creates a new daily archive
- Removes archives older than
RETENTION_DAYS(-k N)
-
pcp-archive-budget cron script (
/etc/cron.daily/pcp-archive-budget):- Checks total archive size per host
- If over
MAX_SIZE_GB, removes oldest archive sets until under budget
# Check archive size
du -sh /var/log/pcp/pmlogger/$(hostname)/
# List archive files
ls -lhS /var/log/pcp/pmlogger/$(hostname)/ | head -20
# Check pmlogger_daily timer
systemctl list-timers | grep pmlog
# Check retention config
cat /etc/default/pmlogger # or /etc/sysconfig/pmlogger
# Manually trigger budget enforcement (safe to run)
sudo bash /etc/cron.daily/pcp-archive-budgetEdit /etc/default/pmlogger (or /etc/sysconfig/pmlogger):
PMLOGGER_DAILY_PARAMS="-E -k 180 -x 0" # 180 days
PCP_MAX_SIZE_GB=3 # 3 GB budgetThen restart: sudo systemctl restart pmlogger
ls -la /usr/share/cockpit/cockpit-temps/
cat /usr/share/cockpit/cockpit-temps/manifest.json
sudo systemctl restart cockpit.socketWork through this checklist in order:
-
Check that PCP services are running:
systemctl status pmcd pmlogger
-
Check that archives exist and contain data:
ls /var/log/pcp/pmlogger/$(hostname)/ pmrep -a /var/log/pcp/pmlogger/$(hostname)/ -o csv -H -t 60sec \ -S "-10minutes" lmsensors.coretemp_isa_0000.package_id_0
-
Check that the metric names in
sensors.jsonmatch your hardware:pminfo -t lmsensors
If the names differ, update
sensors.jsonand re-runinstall_plugin.sh. -
If archives are empty, wait a few minutes — pmlogger needs time after startup to write the first data points.
# Reinstall the PMDA
cd /var/lib/pcp/pmdas/lmsensors
sudo ./Remove
sudo ./Install
sudo systemctl restart pmcd
pminfo lmsensorsdu -sh /var/log/pcp/pmlogger/$(hostname)/
sudo bash /etc/cron.daily/pcp-archive-budget
sudo sed -i 's/-k [0-9]*/-k 90/' /etc/default/pmlogger
sudo systemctl restart pmloggersensors
# Expected: output showing your hardware adapters (coretemp, nvme, k10temp, etc.)pminfo -t lmsensors
# Expected: list of lmsensors.* metric names matching your hardware# Replace the metric name with one from your pminfo output
pmval -s 3 -t 2sec lmsensors.coretemp_isa_0000.package_id_0
# Expected: 3 temperature samples, e.g. 55.000, 56.000, 55.000# Wait a few minutes after setup, then:
pmrep -a /var/log/pcp/pmlogger/$(hostname)/ -o csv -H -t 60sec \
-S "-10minutes" lmsensors.coretemp_isa_0000.package_id_0
# Expected: CSV rows with timestamps and temperature values- Open Cockpit → Temperatures
- Select a sensor checkbox
- Choose a time preset → click Fetch data
- Expected: line chart renders with data points
- Hover over the chart → tooltip shows timestamp and temperature value
- Threshold lines are visible
sudo reboot
# After reboot:
systemctl is-active pmcd pmlogger cockpit.socket
# Expected: all "active"# Remove plugin only
sudo bash scripts/uninstall_plugin.sh
# Remove plugin + PCP config (archives are preserved)
sudo bash scripts/uninstall_plugin.sh --purgeMIT
I build a small family of Cockpit plugins for home servers, all dependency-light and made to be readable at a glance:
- cockpit-temps — hardware temperature history with thresholds (this plugin)
- cockpit-smart — S.M.A.R.T. disk health with trend tracking
- cockpit-pcloud — pCloud storage quota and backup folder status
- cockpit-tailscale — plain-language Tailscale network overview
Browse them all via the cockpit-plugin topic.
