A HTTP API that wraps output of the upsc command as a JSON output and serves it on an endpoint. Meant to be used in a Docker environment and dependent on nut-upsd to serve as data input.
Everybody knows what it feels like to set up a server or a Pi or a configuration for that matter with a tutorial, but once it crashes or you need to look into a problem later on you don't know what you did anymore. I'm used to working with a microservice based architecture at work, including Docker and Kubernetes and see the benefits of having something similar at home.
I have a UPS (Uninterruptible Power Supply) and would like to have geeky metrics on that without using a lot of custom stuff so I started to look into solutions that I could manage on the long term. My NAS is too old to support it and I didn't want to tincker with it.
My setup includes a Raspberry Pi 4B (4GB) with an SSD and I manage everything through GitOps. For deployment I use Docker Compose. I already have InfluxDB and Grafana running with Telegraf to collect metrics so I figured it should be easy to add my UPS metrics ...
NUT, or Network UPS Tools is a project that provides control and monitoring features with a uniform control and management interface compatible with up to several thousand models.
I found a NUT server container for arm: https://hub.docker.com/r/instantlinux/nut-upsd-arm32 which takes off the load of manual configuring the services. With only needing to map the device and a description. You can find the possible variables here: https://github.com/instantlinux/docker-tools/tree/master/images/nut-upsd.
Cool, so that's working.
I found a Python script that could work with the Telegraf [[inputs.exec]] plugin but it only works if you have upsc installed on the same system, which was not the case since I am running this in Docker.
I'm pretty familiar with API's so I decided to make one in Node.js, it's not only a good way to scrape them for metrics but I can always manually get a status. There is probably room for improvement and didn't want to overkill it.
Next, I found a git repository that is a fork of the Node-NUT NPM package that is brought up-to-date with promises. However, upon using it I needed some changes done to the lib so I decided to just include a copy in my own project.
By default the output is just returned from the NUT service with everything being a string. I prefer a more valid JSON output and have the values parsed as their proper types. You can include ?parsed=true to both endpoints and I would advise using this.
http://<server-address>:<server-port>/devices
Retrieve a list of configured UPS devices from the NUT service
example without parsed (http://192.168.1.100:3001/devices):
{
    "ups": "APC Back-UPS XS 950U"
}example with parsed (http://192.168.1.100:3001/devices?parsed=true):
[
    {
        "name": "ups",
        "description": "APC Back-UPS XS 950U"
    }
]In both cases ups is the name you have configured in nut-upsd as environment variable.
http://<server-address>:<server-port>/devices/<device-name>
Retrieve the specified device values from the NUT service
example without parsed: (http://192.168.1.100:3001/devices/ups):
{
    "battery.charge": "100",
    "battery.charge.low": "10",
    "battery.charge.warning": "50",
    "battery.date": "2001/09/25",
    "battery.mfr.date": "2020/02/16",
    "battery.runtime": "3180",
    "battery.runtime.low": "120",
    "battery.type": "PbAc",
    "battery.voltage": "13.6",
    "battery.voltage.nominal": "12.0",
    "device.mfr": "American Power Conversion",
    "device.model": "Back-UPS XS 950U  ",
    "device.serial": "[REDACTED]  ",
    "device.type": "ups",
    "driver.name": "usbhid-ups",
    "driver.parameter.pollfreq": "30",
    "driver.parameter.pollinterval": "2",
    "driver.parameter.port": "auto",
    "driver.parameter.serial": "[REDACTED]",
    "driver.parameter.synchronous": "no",
    "driver.version": "3.8.0-3727-geade014bef",
    "driver.version.data": "APC HID 0.96",
    "driver.version.internal": "0.41",
    "input.sensitivity": "medium",
    "input.transfer.high": "280",
    "input.transfer.low": "155",
    "input.transfer.reason": "input voltage out of range",
    "input.voltage": "240.0",
    "input.voltage.nominal": "230",
    "ups.beeper.status": "enabled",
    "ups.delay.shutdown": "20",
    "ups.firmware": "925.T2 .I",
    "ups.firmware.aux": "T2 ",
    "ups.load": "11",
    "ups.mfr": "American Power Conversion",
    "ups.mfr.date": "2020/02/16",
    "ups.model": "Back-UPS XS 950U  ",
    "ups.productid": "0002",
    "ups.realpower.nominal": "480",
    "ups.serial": "[REDACTED]  ",
    "ups.status": "OL",
    "ups.test.result": "No test initiated",
    "ups.timer.reboot": "0",
    "ups.timer.shutdown": "-1",
    "ups.vendorid": "051d"
}example with parsed: (http://192.168.1.100:3001/devices/ups?parsed=true):
{
    "battery": {
        "charge": 100,
        "date": "2001/09/25",
        "mfr": {
            "date": "2020/02/16"
        },
        "runtime": 3352,
        "type": "PbAc",
        "voltage": 13.4
    },
    "device": {
        "mfr": "American Power Conversion",
        "model": "Back-UPS XS 950U",
        "serial": "[REDACTED]",
        "type": "ups"
    },
    "driver": {
        "name": "usbhid-ups",
        "parameter": {
            "pollfreq": 30,
            "pollinterval": 2,
            "port": "auto",
            "serial": "[REDACTED]",
            "synchronous": "no"
        },
        "version": "3.8.0-3727-geade014bef"
    },
    "input": {
        "sensitivity": "medium",
        "transfer": {
            "high": 280,
            "low": 155,
            "reason": "input voltage out of range"
        },
        "voltage": 242
    },
    "ups": {
        "beeper": {
            "status": "enabled"
        },
        "delay": {
            "shutdown": 20
        },
        "firmware": "925.T2 .I",
        "load": 11,
        "mfr": "American Power Conversion",
        "model": "Back-UPS XS 950U",
        "productid": 2,
        "realpower": {
            "nominal": 480
        },
        "serial": "[REDACTED]",
        "status": "OL",
        "test": {
            "result": "No test initiated"
        },
        "timer": {
            "reboot": 0,
            "shutdown": -1
        },
        "vendorid": "051d",
        "statusnum": 1
    }
}Note: In the parsed object I have added one extra property called statusnum, this is so I can map the value in Grafana more easily. This is based on my device's statusses. If needed I can make the mapping overwritable as an environment setting.
| Status | Number | Description | 
|---|---|---|
| OL | 1 | Online | 
| OL CHRG | 2 | Online & Charging | 
| OL CHRG LB | 2 | Online Low Battery | 
| OB DISCHRG | 3 | On Battery | 
| LB | 4 | Low Battery | 
| SD | 5 | Shutdown Load | 
You can run the code by:
- Make sure you have nodejs installed on your system
 - Run 
npm install(only needed the first time) - Run 
npm start 
You should be able to build the Docker container locally but take note of the cpu architecture you are building it on.
docker build -t deetoreu/nut-http:latest .
The docker container is available on Docker Hub: https://hub.docker.com/r/deetoreu/nut-http
These variables can be passed to the image from kubernetes.yaml or docker-compose.yml as needed:
| Variable | Type | Default | Description | 
|---|---|---|---|
| LOG_LEVEL | String | DEBUG | log4js debug level, choices are: OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, ALL, but I reccomend keeping it on DEBUG | 
| SERVER_PORT | Number | 3001 | The port on which to expose this API | 
| SERVER_TIMEOUT | Number | 2 * 60 * 1000 | Global response timeout for incoming HTTP calls in [ms] | 
| NUT_ADDRESS | String | The address of your NUT process | |
| NUT_PORT | Number | 3493 | The port on which the NUT process is exposed | 
| LOCK_TIMEOUT | Number | 1000 | When you have more than 1 UPS and you are using Telegraf to scrape the data at the same time this will avoid errors from an already running thread, in [ms] | 
If there would be a need I can always include usename & password to access the NUT server.
Everything is logged on the console but also saved to a file per day.
If you want your logs to be persistent you can map a volume to /usr/src/app/logs
docker run example:
docker run -p 3001:3001 -e NUT_ADDRESS=192.168.1.100 -v ~/Documents/tmp/logs:/usr/src/app/logs deetoreu/nut-http:latest
or add the detach -d flag to run in the background
docker-compose.yml example:
version: '3.6'
services:
  nut-http:
    container_name: nut-http
    image: deetoreu/nut-http:latest
    restart: unless-stopped
    ports:
      - 3001:3001
    environment:
      NUT_ADDRESS: "192.168.1.100"
    volumes:
      - ./volumes/nut-http/logs:/usr/src/app/logsapply with docker-compose -f docker-compose.yml up -d
As mentioned I use Telegraf for metrics, you can now easily use the [[inputs.http]] plugin.
Documentation can be found here: https://docs.influxdata.com/telegraf/v1.14/guides/using_http/
Make sure you have set up Telegraf and Influxdb correctly.
telegraf.conf example:
[[inputs.http]]
  urls = [
    "http://192.168.1.100:3001/devices/ups?parsed=true"
  ]
  data_format = "json"
  name_override = "ups"
  tagexclude = ["url", "host"]
  fielddrop = ["driver_parameter_pollfreq", "driver_parameter_pollinterval", "ups_productid"]
  json_string_fields = ["ups_model", "ups_status", "ups_beeper_status"]
To Visualise the data in Grafana I made the following dashboard based on all the previous names used: https://grafana.com/grafana/dashboards/12205
Feel free to add your comments, report issues or make a PR to the project.
I hope this was of some help to at least someone else.