Skip to content

An API wrapper that outputs the upsc command as a JSON output

License

Notifications You must be signed in to change notification settings

dietah/nut-http

Repository files navigation

nut-http

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.

Backstory

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

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.

Metrics

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.

API

Query parameters

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.

Endpoints

Devices

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.

Device values

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

Run locally

You can run the code by:

  1. Make sure you have nodejs installed on your system
  2. Run npm install (only needed the first time)
  3. Run npm start

Docker

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 .

Deployment

The docker container is available on Docker Hub: https://hub.docker.com/r/deetoreu/nut-http

Environment variables

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.

Log files

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

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/logs

apply with docker-compose -f docker-compose.yml up -d

Metrics

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"]

Dashboard

To Visualise the data in Grafana I made the following dashboard based on all the previous names used: https://grafana.com/grafana/dashboards/12205

nut-http dashboard

Add to the project

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.