Skip to content

Instantly share code, notes, and snippets.

@877dev
Last active March 18, 2021 15:38
Show Gist options
  • Save 877dev/0b0f411aef84408a9b860db2ccdb25ea to your computer and use it in GitHub Desktop.
Save 877dev/0b0f411aef84408a9b860db2ccdb25ea to your computer and use it in GitHub Desktop.
Setting up APC UPS monitoring with 'Dockerised' Node Red
[{"id":"e9f6057b.583898","type":"exec","z":"42f852c3.941c8c","command":"ssh [email protected] /sbin/apcaccess","addpay":false,"append":"","useSpawn":"false","timer":"","name":"apcaccess","x":330,"y":1080,"wires":[["fdf663d5.5d4a9","73e4509a.7bd2e","7727397d.4c9bb8"],[],[]]},{"id":"2bdb7bf1.6e87d4","type":"inject","z":"42f852c3.941c8c","name":"every 30 seconds","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"30","crontab":"","once":true,"onceDelay":"","topic":"","payload":"","payloadType":"str","x":140,"y":1080,"wires":[["e9f6057b.583898"]]},{"id":"3c80b3fe.1ed81c","type":"comment","z":"42f852c3.941c8c","name":"Extract data from a connected APC UPS via apcupsd and post to mqtt","info":"","x":610,"y":980,"wires":[]},{"id":"fdf663d5.5d4a9","type":"change","z":"42f852c3.941c8c","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":600,"y":1260,"wires":[["8664dbea.bb4708"]]},{"id":"e2df3b8c.b68138","type":"debug","z":"42f852c3.941c8c","name":"Consolidated Output with string values","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":910,"y":1040,"wires":[]},{"id":"73e4509a.7bd2e","type":"function","z":"42f852c3.941c8c","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,"initialize":"","finalize":"","x":570,"y":1070,"wires":[["e2df3b8c.b68138","66e9a107.24924"]]},{"id":"a06724.35e8f8e","type":"comment","z":"42f852c3.941c8c","name":"Latest timestamp","info":"","x":1110,"y":1260,"wires":[]},{"id":"8664dbea.bb4708","type":"debug","z":"42f852c3.941c8c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":920,"y":1260,"wires":[]},{"id":"1e71d694.3ca599","type":"function","z":"42f852c3.941c8c","name":"Format for influxDB","func":"//Convert the \"status\" payload to 1 or 0 value that influxdb can handle\nvar status = msg.payload.status === \"ONLINE\" ? 1 : 0\n\n\n//Now configure the payload as normal for influxDB\nmsg.payload = [\n {\n //See https://linux.die.net/man/8/apcaccess for details\n onlineStatus: status,\n mainsVolts: msg.payload.linev,\n loadPercent: msg.payload.loadpct,\n batteryCharge: msg.payload.bcharge,\n timeLeftMins: msg.payload.timeleft,\n sensitivity: msg.payload.sense,\n batteryVolts: msg.payload.battv,\n noOfTransfers: msg.payload.numxfers, //no of time transferred to battery backup\n secsCurrentBattery: msg.payload.tonbatt, \n secsTotalBattery: msg.payload.cumonbatt,\n },\n {\n topic: \"apcupsd/myUPS\",\n make: \"APC\",\n location: \"Server room\",\n connection: \"ethernet\",\n power: \"mains\",\n friendlyname: \"APC_UPS\"\n }\n];\n\n\n\n//Filter any \"undefined\" parts of the payload - stops influxdb errors\nlet obj = msg.payload[0] // this saves typing\nfor (let field in obj) \n {\n if (obj.hasOwnProperty(field)) \n {\n if (typeof obj[field] === \"undefined\") delete obj[field]\n }\n }\n\n\n//Now return the message\nreturn msg;\n\n\n\n\n\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1110,"y":1100,"wires":[["a57339bc.3171b8"]]},{"id":"66e9a107.24924","type":"function","z":"42f852c3.941c8c","name":"Function parse numbers","func":"//This function from TotallyInformation converts the numerical\n//payloads from strings to numbers, so influxDB can use them.\n\n//https://discourse.nodered.org/t/ups-advice-and-node-red/41836/11\n\nconst out = {}\nObject.keys(msg.payload).forEach( key => {\n let pSplit = msg.payload[key].split(' ')\n \n out[key] = parseFloat(pSplit[0])\n if ( Number.isNaN(out[key]) ) out[key] = pSplit[0]\n})\nmsg.payload = out\nreturn msg","outputs":1,"noerr":0,"initialize":"","finalize":"","x":870,"y":1100,"wires":[["1e71d694.3ca599"]]},{"id":"a57339bc.3171b8","type":"debug","z":"42f852c3.941c8c","name":"To influxDB node","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"auto","x":1340,"y":1100,"wires":[]},{"id":"23838131.50c24e","type":"comment","z":"42f852c3.941c8c","name":"Is the UPS online","info":"","x":1110,"y":1210,"wires":[]},{"id":"eac53daf.2e05d","type":"debug","z":"42f852c3.941c8c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":920,"y":1210,"wires":[]},{"id":"7727397d.4c9bb8","type":"trigger","z":"42f852c3.941c8c","name":"$online?","op1":"true","op2":"false","op1type":"bool","op2type":"bool","duration":"1","extend":true,"overrideDelay":false,"units":"min","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":580,"y":1210,"wires":[["8ab7a62f.212078"]]},{"id":"8ab7a62f.212078","type":"change","z":"42f852c3.941c8c","name":"$online?","rules":[{"t":"change","p":"topic","pt":"msg","from":"^(.*)$","fromt":"re","to":"$1/$online","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":720,"y":1210,"wires":[["eac53daf.2e05d"]]}]

End goal

To have a APC UPS system connected via USB to a Raspberry Pi, and access the UPS data via Node Red running inside a Docker container.

Steps we will take:

  1. Install apcupsd to the host Pi and confirm working
  2. Testing of the UPS unit (optional)
  3. Allow Node Red to execute commands on the host Pi (only needed when using Dockered Node Red)
  4. Use the exec node to call apcupsd and get the UPS data

Step 1 - Installing apcupsd:

Optional - make sure your Pi is up to date:
sudo apt-get update get the latest list of packages
sudo apt-get upgrade upgrade all packages on the Pi

Then install:
sudo apt-get install apcupsd

If you get an arror "Warning communications lost with UPS", just exit with ctrl + c. This is because we haven't set up the config files yet.

Edit config files:
sudo nano /etc/default/apcupsd and change to 'ISCONFIGURED=yes'

sudo nano /etc/apcupsd/apcupsd.conf and enable the following lines:

UPSNAME myups
UPSCABLE usb
UPSTYPE usb
DEVICE

(Note, comment out the line DEVICE /dev/ttyS0 with #). There are many other settings to play with too.

Restart apcupsd for changes to take effect:
sudo apcupsd restart

Check the status to confirm all is working ok:
apcaccess status

If you get STATUS : COMMLOST then you might need to reboot the Pi with sudo reboot

Step 2 - Testing the UPS (optional)

First stop apcupsd with:
sudo service apcupsd stop

Run the test program:
sudo apctest

Here you can test the UPS, read values, set values etc.... When finished press q to quit, and start apcupsd again with sudo service apcupsd start

Step 3 - Set up Node Red to execute commmands on the host (only needed for Dockered Node Red installs)

This is a very cut down version of the excellent guide found below, I highly recommend reading that in full:
https://gist.github.com/877dev/fa67ee47341a0a04d2253598f32a9474

Note this applies to users of IOTstack, you may need to tailor this to suit your Docker setup

However in the spirit of a quicky and dirty TLDR, you will need the following information (defaults shown below):

HOSTADDR = 192.168.1.95 (i.e. your Pi's IP address - make sure it is static)
USERID = pi
Your normal login password

Edit docker-compose.yml:
cd ~/IOTstack sudo nano ~/IOTstack/docker-compose.yml

Find the node red section and add this line to the volumes list:
- ./volumes/nodered/ssh:/root/.ssh

Rebuild node red container with the new directory mapping:
docker-compose up -d nodered

Generate SSH key-pair for Node Red inside the container:
docker exec -it nodered ssh-keygen -t ed25519 and press Enter at every prompt.

Exchange keys with the target host (i.e. the Pi) with the command "docker exec -it nodered ssh-copy-id «USERID»@«HOSTADDR»"
For me the command is:
docker exec -it nodered ssh-copy-id [email protected]
Type yes when prompted and press enter, ignore any errors.
Enter you login password and press enter, it should say "keys added (1)".

Prove that you can now access the root filesystem from within the Node Red Docker container:
docker exec -it nodered ssh [email protected] ls -1 /home/pi/IOTstack
If you see the contents listed, it worked!

Step 4 - Use the exec node to call apcupsd and get the data into Node Red

This is based on TotallyInformation's flow from here: https://flows.nodered.org/flow/cf9813fbca341607a73786c31df362c9
However as we are using a Docker container, a slight change is required.

Import the following example flow into Node Red:

[{"id":"e9f6057b.583898","type":"exec","z":"42f852c3.941c8c","command":"ssh [email protected] /sbin/apcaccess","addpay":false,"append":"","useSpawn":"false","timer":"","name":"apcaccess","x":330,"y":1080,"wires":[["fdf663d5.5d4a9","73e4509a.7bd2e","7727397d.4c9bb8"],[],[]]},{"id":"2bdb7bf1.6e87d4","type":"inject","z":"42f852c3.941c8c","name":"every 30 seconds","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"30","crontab":"","once":true,"onceDelay":"","topic":"","payload":"","payloadType":"str","x":140,"y":1080,"wires":[["e9f6057b.583898"]]},{"id":"3c80b3fe.1ed81c","type":"comment","z":"42f852c3.941c8c","name":"Extract data from a connected APC UPS via apcupsd and post to mqtt","info":"","x":610,"y":980,"wires":[]},{"id":"fdf663d5.5d4a9","type":"change","z":"42f852c3.941c8c","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":600,"y":1260,"wires":[["8664dbea.bb4708"]]},{"id":"e2df3b8c.b68138","type":"debug","z":"42f852c3.941c8c","name":"Consolidated Output with string values","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":910,"y":1040,"wires":[]},{"id":"73e4509a.7bd2e","type":"function","z":"42f852c3.941c8c","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,"initialize":"","finalize":"","x":570,"y":1070,"wires":[["e2df3b8c.b68138","66e9a107.24924"]]},{"id":"a06724.35e8f8e","type":"comment","z":"42f852c3.941c8c","name":"Latest timestamp","info":"","x":1110,"y":1260,"wires":[]},{"id":"8664dbea.bb4708","type":"debug","z":"42f852c3.941c8c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":920,"y":1260,"wires":[]},{"id":"1e71d694.3ca599","type":"function","z":"42f852c3.941c8c","name":"Format for influxDB","func":"//Convert the \"status\" payload to 1 or 0 value that influxdb can handle\nvar status = msg.payload.status === \"ONLINE\" ? 1 : 0\n\n\n//Now configure the payload as normal for influxDB\nmsg.payload = [\n    {\n        //See https://linux.die.net/man/8/apcaccess for details\n        onlineStatus: status,\n        mainsVolts: msg.payload.linev,\n        loadPercent: msg.payload.loadpct,\n        batteryCharge: msg.payload.bcharge,\n        timeLeftMins: msg.payload.timeleft,\n        sensitivity: msg.payload.sense,\n        batteryVolts: msg.payload.battv,\n        noOfTransfers: msg.payload.numxfers, //no of time transferred to battery backup\n        secsCurrentBattery: msg.payload.tonbatt, \n        secsTotalBattery: msg.payload.cumonbatt,\n    },\n    {\n        topic: \"apcupsd/myUPS\",\n        make: \"APC\",\n        location: \"Server room\",\n        connection: \"ethernet\",\n        power: \"mains\",\n        friendlyname: \"APC_UPS\"\n    }\n];\n\n\n\n//Filter any \"undefined\" parts of the payload - stops influxdb errors\nlet obj = msg.payload[0]        // this saves typing\nfor (let field in obj) \n    {\n    if (obj.hasOwnProperty(field)) \n        {\n        if (typeof obj[field] === \"undefined\") delete obj[field]\n        }\n    }\n\n\n//Now return the message\nreturn msg;\n\n\n\n\n\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1110,"y":1100,"wires":[["a57339bc.3171b8"]]},{"id":"66e9a107.24924","type":"function","z":"42f852c3.941c8c","name":"Function parse numbers","func":"//This function from TotallyInformation converts the numerical\n//payloads from strings to numbers, so influxDB can use them.\n\n//https://discourse.nodered.org/t/ups-advice-and-node-red/41836/11\n\nconst out = {}\nObject.keys(msg.payload).forEach( key => {\n    let pSplit = msg.payload[key].split(' ')\n    \n    out[key] = parseFloat(pSplit[0])\n    if ( Number.isNaN(out[key]) ) out[key] = pSplit[0]\n})\nmsg.payload = out\nreturn msg","outputs":1,"noerr":0,"initialize":"","finalize":"","x":870,"y":1100,"wires":[["1e71d694.3ca599"]]},{"id":"a57339bc.3171b8","type":"debug","z":"42f852c3.941c8c","name":"To influxDB node","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"auto","x":1340,"y":1100,"wires":[]},{"id":"23838131.50c24e","type":"comment","z":"42f852c3.941c8c","name":"Is the UPS online","info":"","x":1110,"y":1210,"wires":[]},{"id":"eac53daf.2e05d","type":"debug","z":"42f852c3.941c8c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":920,"y":1210,"wires":[]},{"id":"7727397d.4c9bb8","type":"trigger","z":"42f852c3.941c8c","name":"$online?","op1":"true","op2":"false","op1type":"bool","op2type":"bool","duration":"1","extend":true,"overrideDelay":false,"units":"min","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":580,"y":1210,"wires":[["8ab7a62f.212078"]]},{"id":"8ab7a62f.212078","type":"change","z":"42f852c3.941c8c","name":"$online?","rules":[{"t":"change","p":"topic","pt":"msg","from":"^(.*)$","fromt":"re","to":"$1/$online","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":720,"y":1210,"wires":[["eac53daf.2e05d"]]}]

Edit the exec node with your details and deploy, all being well you will get your UPS information in the debug tab.
Note I have added some functions to format the payload to be more compatible for influxDB if you use that.

Further reading

http://www.apcupsd.org/ Project homepage
https://www.raspberrypi.org/forums/viewtopic.php?t=26713 Basic setup and web gui interface with Apache
https://github.com/bgulla/apcupsd-mqtt An alternate way of getting MQTT data from apcupsd

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment