Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nut-ups-prometheus-grafana

An Ansible role plus a Grafana dashboard for monitoring a USB UPS through Network UPS Tools (NUT), without a dedicated network-facing exporter. Built for cheap Megatec/Qx-protocol boards (the nutdrv_qx driver) commonly found under budget UPS brands — ExeGate, Ippon, PowerCom, Forza and similar OEM relabels — where the control board often doesn't report battery.charge at all, only battery.voltage.

image image

Why not just use an existing exporter?

Most NUT-to-Prometheus setups assume upsd is reachable over the network and point a separate exporter pod/process at it (e.g. hon95/nut-exporter, metric prefix network_ups_tools_*). That's overkill for a UPS attached by USB to a single host: upsd, the driver, and everything else can just run locally with MODE=standalone and listen on loopback only.

This role instead:

  1. Installs and configures nut-server (upsd + nutdrv_qx) as a normal systemd service, listening on 127.0.0.1 only.
  2. Deploys a small Python script + systemd timer that polls the UPS via upsc every 30 seconds (configurable) and writes the result as a .prom file into node-exporter's textfile collector directory.

Whatever already scrapes node-exporter on that host — Prometheus, VictoriaMetrics, or anything else — picks the UPS metrics up for free. No extra scrape target, no extra pod, no network exposure of upsd.

UPS --USB--> host: nutdrv_qx + upsd (loopback only)
                     |
                     | upsc, every 30s (systemd timer)
                     v
             nut-textfile-exporter.py
                     |
                     v
     node-exporter textfile collector (.prom file)
                     |
                     v
        Prometheus / VictoriaMetrics scrape
                     |
                     v
                  Grafana

Metric naming scheme

The exporter script doesn't hardcode a list of fields — cheap Qx boards vary wildly in what they report, so it takes whatever upsc returns and maps every numeric NUT variable directly: dots become underscores, the value is passed through as-is.

NUT variable Prometheus metric
battery.charge nut_battery_charge
battery.runtime nut_battery_runtime
input.voltage nut_input_voltage
ups.load nut_ups_load

Whatever your board doesn't report simply doesn't appear — no script changes needed. ups.status is parsed separately into nut_ups_status_flag{ups="...", flag="OL|OB|LB|CHRG|..."}, one series per token, all emitted every poll (0 or 1) so the series stays continuous instead of flapping in and out of existence. Static identity fields (model, serial, driver) land in nut_ups_info{...}=1, and nut_up reports whether the poll itself succeeded.

Battery charge guesstimation

If upsc shows battery.voltage but no battery.charge, nutdrv_qx can estimate the charge linearly between two configured voltages — see man nutdrv_qx, section BATTERY CHARGE GUESSTIMATION. The role's defaults (nut_ups_battery_voltage_nominal/high/low) assume a single 12V SLA cell, the typical setup for this class of hardware; override them for anything else. Note that only .nominal has an override. counterpart in NUT — .high/.low only exist as default.*.

To pick sane values for your board: check battery.voltage while the UPS is on mains power and fully charged (that's your high), and use a typical SLA cutoff (~11.0V for a 12V cell) for low.

Requirements

  • A Debian-family host (the role checks ansible_os_family) with the UPS attached over USB.
  • lsusb output to find your board's idVendor/idProduct — required, there's no sane default.
  • node-exporter already running with --collector.textfile.directory=<nut_textfile_collector_dir> pointed at the same directory this role writes to (default: /var/lib/node_exporter/textfile_collector).
  • Grafana with a Prometheus-compatible datasource already scraping that node-exporter.

Role variables

Variable Default Notes
nut_ups_name ups Section name in ups.conf and the ups= label on every metric. Keep it ASCII.
nut_ups_vendorid (none, required) USB idVendor of the UPS's USB bridge, from lsusb.
nut_ups_productid (none, required) USB idProduct, from lsusb.
nut_ups_description (none, required) Free-text desc = in ups.conf.
nut_ups_battery_voltage_nominal 12 See guesstimation above.
nut_ups_battery_voltage_high 13.7 Fully-charged voltage on mains, single 12V SLA cell.
nut_ups_battery_voltage_low 11.0 Typical cutoff voltage, single 12V SLA cell.
nut_upsd_listen_address 127.0.0.1 upsd bind address.
nut_upsd_listen_port 3493 upsd bind port.
nut_textfile_collector_dir /var/lib/node_exporter/textfile_collector Must match node-exporter's --collector.textfile.directory.
nut_textfile_poll_interval 30s Timer's OnUnitActiveSec — include the unit suffix.

Example playbook

See examples/playbook.yml:

ansible-playbook -i inventory examples/playbook.yml

Grafana dashboard

Import grafana/nut-ups-dashboard.json into Grafana and point it at your Prometheus/VictoriaMetrics datasource (edit the datasource UID if it isn't VictoriaMetrics). It exposes two template variables, Instance and UPS, so one dashboard covers any number of hosts/UPS units reporting through this scheme. Panels for fields your board doesn't report (e.g. ups.load, input.voltage on boards that don't expose them) are expected to stay empty — that's not a collection failure.

Verifying after install

ssh <host> upsc <nut_ups_name>@localhost
ssh <host> cat /var/lib/node_exporter/textfile_collector/nut_ups.prom
curl -sG --data-urlencode 'query=nut_up' http://<prometheus>/api/v1/query

The first command shows what your board actually reports — that tells you whether the "Load" and "Input Voltage" dashboard panels will ever show data for your specific hardware.

License

MIT, see LICENSE.

About

Ansible role + Grafana dashboard for monitoring a USB Megatec/Qx UPS via NUT and node-exporter's textfile collector

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages