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.
- Install apcupsd to the host Pi and confirm working
- Testing of the UPS unit (optional)
- Allow Node Red to execute commands on the host Pi (only needed when using Dockered Node Red)
- Use the exec node to call apcupsd and get the UPS data
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
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!
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.
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