Send APC UPS data to MQTT

Here is an example flow that reads data from an APC UPS (Uninterruptible Power Supply). These are commonly used to keep PC's and servers powered up during short power outages and to allow them to gracefully shut down.

In this case, I have a low-cost APC UPS connected via a USB cable to the Raspberry Pi that is running Node-RED and a Mosquitto MQTT broker.

To get the latest data from the UPS, we are using a command-line Linux tool called apcaccess. This is part of apcupsd which you should be able to install directly from your distribution's software library.

There are 2 function nodes included in the flow. One that isn't wired in, that converts the text output from the tool into an object on the payload containing properties for everything returned by the query. The second one, that is in use, is very similar but throws out a new message for each property using topics that correspond roughly to the homie MQTT topic structure. Homie is similar to a revised topic structure I'd come up with myself so it seemed to make sense to use it. However, I find the full convention rather restrictive and overly complex - so I keep it close rather than fully compliant.

We also create two additional outputs. One tells MQTT whether we consider the UPS to be online (it is talking to the Pi), the other provides an updated timestamp.

The RBE node is used to minimise the number of messages being sent to MQTT - only values that have changed (by topic) will actually be sent.

The output from the flow should be sent to your MQTT broker using an MQTT-Out node.

[
    {
        "id": "a1ba56ae.d63698",
        "type": "exec",
        "z": "9974253c.de8db8",
        "command": "/sbin/apcaccess",
        "append": "",
        "useSpawn": "",
        "name": "apcaccess",
        "x": 330,
        "y": 1020,
        "wires": [
            [
                "d1aef683.b32798",
                "f4621c4b.e5fab",
                "30695dc7.39ecf2"
            ],
            [],
            []
        ]
    },
    {
        "id": "eb7e2830.5ac0f8",
        "type": "inject",
        "z": "9974253c.de8db8",
        "name": "every 50 seconds",
        "topic": "homie/ups1",
        "payload": "",
        "payloadType": "str",
        "repeat": "50",
        "crontab": "",
        "once": true,
        "x": 150,
        "y": 1020,
        "wires": [
            [
                "a1ba56ae.d63698"
            ]
        ]
    },
    {
        "id": "47b75803.e910d8",
        "type": "comment",
        "z": "9974253c.de8db8",
        "name": "Extract data from a connected APC UPS via apcupsd and post to mqtt topic homie/<device-name>/...",
        "info": "",
        "x": 380,
        "y": 940,
        "wires": []
    },
    {
        "id": "f4621c4b.e5fab",
        "type": "trigger",
        "z": "9974253c.de8db8",
        "op1": "true",
        "op2": "false",
        "op1type": "bool",
        "op2type": "bool",
        "duration": "1",
        "extend": true,
        "units": "min",
        "reset": "",
        "name": "$online?",
        "x": 640,
        "y": 980,
        "wires": [
            [
                "a8628094.c4ee3"
            ]
        ]
    },
    {
        "id": "a8628094.c4ee3",
        "type": "change",
        "z": "9974253c.de8db8",
        "name": "$online?",
        "rules": [
            {
                "t": "change",
                "p": "topic",
                "pt": "msg",
                "from": "^(.*)$",
                "fromt": "re",
                "to": "$1/$online",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 780,
        "y": 980,
        "wires": [
            [
                "1c98824f.569f8e"
            ]
        ]
    },
    {
        "id": "30695dc7.39ecf2",
        "type": "change",
        "z": "9974253c.de8db8",
        "name": "$stats/$updated",
        "rules": [
            {
                "t": "change",
                "p": "topic",
                "pt": "msg",
                "from": "^(.*)$",
                "fromt": "re",
                "to": "$1/$stats/$updated",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "$now()\t",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 760,
        "y": 1020,
        "wires": [
            [
                "1c98824f.569f8e"
            ]
        ]
    },
    {
        "id": "4636f564.7fd28c",
        "type": "debug",
        "z": "9974253c.de8db8",
        "name": "Consolidated Output",
        "active": true,
        "console": "false",
        "complete": "true",
        "x": 840,
        "y": 1100,
        "wires": []
    },
    {
        "id": "34bb4063.ad0e9",
        "type": "function",
        "z": "9974253c.de8db8",
        "name": "Turn apcaccess into payload",
        "func": "const ans = {}\n\nArray.prototype.map.call( msg.payload.trim().split(\"\\n\"), function(line) {\n\n    if ( line.trim() === '' ) return\n\n    let stat = line.split(':')\n    \n    // Some values contain ':', when they do, we have to rejoin\n    if ( stat.length > 2 ) {\n        let newStat = []\n        newStat.push( stat.shift() )\n        newStat.push( stat.join(':') )\n        stat = newStat\n    }\n    \n    let label = stat[0].toLowerCase().trim()\n    let value = stat[1].trim()\n\n    ans[label] = value\n    \n    return\n    \n} )\n\nmsg.payload = ans\n\nreturn msg",
        "outputs": 1,
        "noerr": 0,
        "x": 560,
        "y": 1100,
        "wires": [
            []
        ]
    },
    {
        "id": "d1aef683.b32798",
        "type": "function",
        "z": "9974253c.de8db8",
        "name": "Turn apcaccess into homie output",
        "func": "/**\n * Recieves data from apcaccess command line tool that queries\n * apcupsd for data. This works with both network and USB\n * connected UPS's from APC.\n * \n * Author: Julian Knight (Totally Information)\n * Date:   2017-12-02\n */\n \nconst ans = {}\n\n// Send some core information first - doing this manually for simplicity\nnode.send( {\n    \"topic\": `${msg.topic}/$implementation`,\n    \"payload\": \"nrlive/USB\"\n} )\nnode.send( {\n    \"topic\": `${msg.topic}/$location`,\n    \"payload\": \"HOME/IN/03/LOFT\"\n} )\nnode.send( {\n    \"topic\": `${msg.topic}/$type`,\n    \"payload\": \"UPS\"\n} )\nnode.send( {\n    \"topic\": `${msg.topic}/$source`,\n    \"payload\": \"USB\"\n} )\n\n// Walk over every line of the input text\nArray.prototype.map.call( msg.payload.trim().split(\"\\n\"), function(line) {\n\n    if ( line.trim() === '' ) return\n\n    let stat = line.split(':')\n    \n    // Some values contain ':', when they do, we have to rejoin\n    if ( stat.length > 2 ) {\n        let newStat = []\n        newStat.push( stat.shift() )\n        newStat.push( stat.join(':') )\n        stat = newStat\n    }\n    \n    let label = stat[0].toLowerCase().trim()\n    let value = stat[1].trim()\n\n    ans[label] = value\n    \n    // homie wants everything in separate outputs\n    // and some properties are $stats or other special entries\n    switch ( label ) {\n        // Ignore these\n        case 'hostname': // host the ups is attached to\n        case 'version':  // the hosts Linux version\n        case 'upsname':  // seems to be the hosts name\n        case 'driver':\n        case 'end apc':  // date/time query ended\n        case 'date':     // the UPS internal date/time\n            break;\n            \n        case 'firmware':\n            node.send( {\n                \"topic\": `${msg.topic}/$fw/version`,\n                \"payload\": value\n            } )\n            break;\n            \n        case 'model':\n            node.send( {\n                \"topic\": `${msg.topic}/$fw/name`,\n                \"payload\": value\n            } )\n            node.send( {\n                \"topic\": `${msg.topic}/$name`,\n                \"payload\": value\n            } )\n            break;\n            \n        // Date and time apcupsd was started\n        case 'starttime':\n            // Uptime is in seconds\n            let uptime = ( (new Date()) - (new Date(value)) ) / 1000\n            node.send( {\n                \"topic\": `${msg.topic}/$stats/uptime`,\n                \"payload\": uptime\n            } )\n            node.send( {\n                \"topic\": `${msg.topic}/starttime`,\n                \"payload\": (new Date(value)).toISOString()\n            } )\n            break;\n        \n        // Everything else can be sent straight on\n        default:\n            node.send( {\n                \"topic\": `${msg.topic}/${label}`,\n                \"payload\": value\n            } )\n            break;\n            \n    }\n    \n    return\n    \n} )\n\n// Send the consolidated output to port 2\nmsg.payload = ans\nreturn [null, msg]",
        "outputs": "2",
        "noerr": 0,
        "x": 580,
        "y": 1060,
        "wires": [
            [
                "e5e61884.eaa738"
            ],
            [
                "4636f564.7fd28c"
            ]
        ],
        "outputLabels": [
            "homie output",
            "Consolidated output"
        ]
    },
    {
        "id": "e5e61884.eaa738",
        "type": "rbe",
        "z": "9974253c.de8db8",
        "name": "",
        "func": "rbe",
        "gap": "",
        "start": "",
        "inout": "out",
        "x": 790,
        "y": 1060,
        "wires": [
            [
                "1c98824f.569f8e"
            ]
        ]
    }
]
TotallyInformation

Flow Info

created 1 week, 3 days ago

Node Types

Core
  • change (x2)
  • comment (x1)
  • debug (x1)
  • exec (x1)
  • function (x2)
  • inject (x1)
  • trigger (x1)
Other

Tags

  • mqtt
  • ups
  • apc
  • homie
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option