├── .gitignore ├── README.md ├── index.js ├── package.json ├── test.js └── types.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Homebridge-nut 2 | [![NPM Version](https://img.shields.io/npm/v/homebridge-nut.svg)](https://www.npmjs.com/package/homebridge-nut) 3 | 4 | NUT (Network UPS Tools) Plugin for [Homebridge](https://github.com/nfarina/homebridge) leveraging [node-nut](https://github.com/skarcha/node-nut). 5 | 6 | This plugin allows you to monitor your UPS's with HomeKit and Siri via a NUT Client. 7 | 8 | ## Installation 9 | 1. Install homebridge using: `npm install -g homebridge` 10 | 2. Install this plugin using: `npm install -g homebridge-nut` 11 | 3. Update your configuration file like the example below. 12 | 4. Ensure you have a NUT Client running somewhere. For assistance - http://wynandbooysen.com/raspberry-pi-ups-server-using-nut.html. 13 | 14 | This plugin will create a ContactSensor element with BatteryService for each USB returned from your NUT Client. 15 | * ContactSensorState will show OPEN if UPS Status starts with OB (On Battery). 16 | * StatusActive will be true if UPS Load is > 0. 17 | * StatusFault will be true is NUT is not reachable. 18 | * BatteryLevel will show the BatteryCharge percent. 19 | * ChargingState will show Charging, Not Charging (Online and 100%), or Not Chargable (On Battery). 20 | * StatusLowBattery will be true if low_batt_threshold is breached. This can potentially notify you prior to your Nut shutting down its server(s). 21 | * Input/Output/Battery Voltage & BatteryLoad will be shown if available, otherwise may show 0 or default values. 22 | 23 | ## Configuration 24 | Example config.json: 25 | 26 | ```js 27 | "platforms": [ 28 | { 29 | "platform": "Nut", 30 | "name": "Nut", 31 | "host": "localhost", 32 | "port": "3493", 33 | "search_time_delay": "1", 34 | "acc_delay": "100", 35 | "low_batt_threshold": "40", 36 | "polling": "120" 37 | } 38 | ] 39 | ``` 40 | 41 | ## Explanation: 42 | 43 | Field | Description 44 | ------------------------|------------ 45 | **platform** | Required - Must always be "Nut". 46 | **name** | Required - Name for platform logging. 47 | **host** | Optional - Internal ip or hostname of Nut Client. Default is localhost. 48 | **port** | Optional - Port which Nut Client is listening. Default is 3493. 49 | **search_time_delay** | Optional - Delay on startup to list Nut devices. Defaults to 1 second. 50 | **acc_delay** | Optional - Delay to prevent communication collisions for multiple UPS accessories. Defaults to 100 milliseconds. 51 | **low_batt_warning** | Optional - Percent at which UPS will raise low battery. Default is 40. 52 | **polling** | Optional - Poll interval. Default is 0 sec, which is OFF or no polling. 53 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Nut (Network UPS Tool) Platform for HomeBridge 4 | // 5 | // Remember to add platform to config.json. Example: 6 | // "platforms": [ 7 | // { 8 | // "platform": "Nut", // required 9 | // "name": "Nut", // required - Used for platform logging tags. 10 | // "host": "localhost", // Optional - defaults to localhost 11 | // "port": "3493", // Optional - defaults to 3493 12 | // "search_time_delay": "1", // Optional - defaults to 1 second. Initial search time delay in seconds. 13 | // "acc_delay": "100", // Optional - defaults to 100. Time in milliseconds. Delay to prevent communication collisions for multiple UPS accessories. 14 | // "low_batt_threshold": "40", // Optional - defaults to 40%. Low Battery Threshold. 15 | // "polling": "120" // Optional - defaults to OFF or 0. Time in seconds. 16 | // } 17 | // ], 18 | // 19 | // 20 | 21 | var Nut = require("node-nut"); 22 | var _ = require("underscore"); 23 | var sleep = require("system-sleep") 24 | var titleCase = require('title-case'); 25 | var pNut, connected, started; 26 | 27 | function NutPlatform(log, config){ 28 | this.config = config; 29 | this.host = config["host"] || 'localhost'; 30 | this.port = config["port"] || '3493'; 31 | this.name = config["name"]; 32 | this.nutListTimeout = parseInt(config["search_time_delay"] || '1'); 33 | this.nutListTimeout *= 1000; 34 | this.accDelay = parseInt(config["acc_delay"] || '100'); 35 | this.nutPolling = parseInt(config["polling"] || '0'); 36 | this.lowBattThreshold = config["low_batt_threshold"] || '40'; 37 | this.log = log; 38 | this.log("Starting Nut Platform on %s:%s. Polling (seconds): %s", this.host, this.port, (this.nutPolling == '0') ? 'OFF' : this.nutPolling); 39 | this.nutPolling *=1000; 40 | this.initialized = false; 41 | var nutAccessories, upsInfo; 42 | connected = false; 43 | started = true; 44 | 45 | pNut = new Nut(this.port, this.host); 46 | 47 | pNut.on('ready', this.eventReady.bind(this)); 48 | pNut.on('close', this.eventClose.bind(this)); 49 | pNut.on('error', this.eventError.bind(this)); 50 | 51 | pNut.start(); 52 | } 53 | 54 | NutPlatform.prototype = { 55 | accessories: function(callback) { 56 | var that = this; 57 | var foundAccessories = []; 58 | var acc, accName, accFriendlyName; 59 | sleep(this.nutListTimeout); // Wait for initial nut startup. 60 | accName = _.keys(that.nutAccessories); 61 | accFriendlyName = _.values(that.nutAccessories); 62 | for (acc in accName) { 63 | var titleName; 64 | that.log("Received Nut accessory #%s %s (%s) from the %s platform.", acc, accName[acc], ((!accFriendlyName[acc]) ? 'NullDesc' : accFriendlyName[acc]), that.name); 65 | that.getInfo(accName[acc], function(err, upsInfo) { 66 | if (!err) { 67 | titleName = titleCase((accFriendlyName[acc] || accName[acc])); //Added in case ups.conf description is empty. 68 | var accessory = new NutAccessory(that, accName[acc], titleName, upsInfo, acc); 69 | that.initialized = true; 70 | foundAccessories.push(accessory); 71 | } else { 72 | that.log.error("Nut Error: %s", err); 73 | } 74 | }); 75 | sleep(this.accDelay); // Delay on each new accessory call to avoid collisions. 76 | } 77 | callback(foundAccessories); 78 | }, 79 | 80 | eventReady: function() { 81 | var that = this; 82 | if (!that.initialized) { 83 | connected = true; 84 | this.log.debug("Nut eventReady received. Initializing and getting list of Nut accessories."); 85 | pNut.GetUPSList(function(upslist, err) { 86 | if (err) { 87 | that.log.error;('Nut ERROR initializing: ' + err); 88 | } 89 | else { 90 | that.nutAccessories = upslist; 91 | } 92 | }); 93 | } else { 94 | connected = true; 95 | this.log.debug("Nut eventReady received. Successful reconnect after disconnection."); 96 | } 97 | }, 98 | 99 | eventClose: function() { 100 | var that = this; 101 | connected = false; 102 | started = false; 103 | this.log.debug("Nut eventDisconnect occured."); 104 | }, 105 | 106 | eventError: function(error) { 107 | this.log.error("Nut eventError received - %s.", error); 108 | }, 109 | 110 | getInfo: function(upsName, callback) { 111 | var that = this; 112 | if (connected) { 113 | pNut.GetUPSVars(upsName, function(upsvars, err) { 114 | if (err) { 115 | callback("ERROR getting UPSVars: " + err, null); 116 | } else { 117 | callback(null, upsvars); 118 | } 119 | }); 120 | } else { 121 | callback("ERROR not connected to Nut", null); 122 | } 123 | } 124 | } 125 | 126 | function NutAccessory(platform, accessory, accessoryFriendly, accessoryVars, accCount) { 127 | this.nutName = accessory; 128 | this.name = accessoryFriendly; 129 | this.accVars = accessoryVars; 130 | this.platform = platform; 131 | this.log = this.platform.log; 132 | this.lowBattThreshold = this.platform.lowBattThreshold; 133 | this.delay = (parseInt(accCount)+1)*this.platform.accDelay; 134 | this.log.debug("Nut Platform polling delay is: %s, Accessory %s delay is: %s", this.platform.nutPolling/1000, this.name, this.delay/1000); 135 | if (this.platform.nutPolling > 0) { 136 | var that = this; 137 | setTimeout(function() { 138 | that.log.debug('Nut Service Polling begin for %s...', that.name); 139 | that.servicePolling(); 140 | }, this.platform.nutPolling); 141 | }; 142 | } 143 | 144 | NutAccessory.prototype = { 145 | 146 | identify: function(callback){ 147 | this.log.debug("Nut Identify requested for %s!", this.name); 148 | callback(); 149 | }, 150 | 151 | getCheck: function(callback){ 152 | var that = this; 153 | this.log.debug('Nut checking connection to Nut Server for %s.', this.name); 154 | if (connected) { 155 | this.log.debug('Nut connection to Nut Server Successful.'); 156 | this.log.debug('Nut refreshing delay %s for %s', this.delay/1000, this.name); 157 | sleep(parseInt(this.delay)); 158 | that.getVars(function(callback) {}.bind(this)); 159 | } else { 160 | if (!started) { 161 | started = true; 162 | this.log.debug('Nut not connected, attempting reconnection.'); 163 | pNut.start(); 164 | sleep(this.platform.nutListTimeout); 165 | } else { 166 | this.log.debug('Nut not connected, waiting on prior reconnection attempt...'); 167 | } 168 | this.log.debug('Nut refreshing delay %s for %s',this.delay/1000, this.name); 169 | sleep(parseInt(this.delay)); 170 | if (connected) { 171 | that.getVars(function (callback) {}.bind(that)); 172 | } else { 173 | that.log.error('Nut unable to reconnect!'); 174 | that.service.setCharacteristic(Characteristic.StatusFault,1); 175 | } 176 | } 177 | callback(); 178 | }, 179 | 180 | getVars: function(callback){ 181 | var that = this; 182 | this.log.debug('Nut now getting vars for %s.', this.name); 183 | if (connected) { 184 | this.platform.getInfo(this.nutName, function(err, upsInfo) { 185 | if (!err) { 186 | this.log.debug('Nut now updating vars for %s.', this.name); 187 | that.serviceInfo.setCharacteristic(Characteristic.Manufacturer, upsInfo["device.mfr"] || upsInfo["ups.vendorid"] || 'No Manufacturer'); 188 | that.serviceInfo.setCharacteristic(Characteristic.SerialNumber, upsInfo["ups.serial"] || 'No Serial#') 189 | that.serviceInfo.setCharacteristic(Characteristic.Model, upsInfo["device.model"] || upsInfo["ups.productid"] || 'No Model#'); 190 | 191 | that.service.setCharacteristic(Characteristic.StatusFault,0); 192 | that.serviceBatt.setCharacteristic(Characteristic.BatteryLevel,parseFloat(upsInfo["battery.charge"])); 193 | that.service.setCharacteristic(EnterpriseTypes.InputVoltageAC, parseFloat(upsInfo["input.voltage"])); 194 | that.service.setCharacteristic(EnterpriseTypes.OutputVoltageAC, parseFloat(upsInfo["output.voltage"])); 195 | that.service.setCharacteristic(EnterpriseTypes.BatteryVoltageDC, parseFloat(upsInfo["battery.voltage"])); 196 | that.service.setCharacteristic(EnterpriseTypes.UPSLoadPercent, parseInt(upsInfo["ups.load"])); 197 | that.service.setCharacteristic(Characteristic.CurrentTemperature, parseFloat(upsInfo["ups.temperature"])); 198 | 199 | if (parseInt(upsInfo["battery.charge"]) < parseInt(that.lowBattThreshold)) { 200 | that.serviceBatt.setCharacteristic(Characteristic.StatusLowBattery,1); 201 | } else { 202 | that.serviceBatt.setCharacteristic(Characteristic.StatusLowBattery,0); 203 | } 204 | if (upsInfo["ups.status"] == "OL CHRG") { 205 | that.serviceBatt.setCharacteristic(Characteristic.ChargingState,1); 206 | } else if (upsInfo["ups.status"] == "OB DISCHRG") { 207 | that.serviceBatt.setCharacteristic(Characteristic.ChargingState,2); 208 | } else { 209 | that.serviceBatt.setCharacteristic(Characteristic.ChargingState,0); 210 | } 211 | if (parseInt(upsInfo["ups.load"]) > 0) { 212 | that.service.setCharacteristic(Characteristic.StatusActive,1); 213 | } else { 214 | that.service.setCharacteristic(Characteristic.StatusActive,0); 215 | } 216 | if (upsInfo["ups.status"].startsWith("OB")) { 217 | that.service.setCharacteristic(Characteristic.ContactSensorState,1); 218 | } else { 219 | that.service.setCharacteristic(Characteristic.ContactSensorState,0); 220 | } 221 | } else { 222 | that.log.error("Nut Error: %s", err); 223 | that.service.setCharacteristic(Characteristic.StatusFault,1); 224 | } 225 | }.bind(this)); 226 | } else { 227 | this.log.error('Nut request failed. Not connected.'); 228 | that.service.setCharacteristic(Characteristic.StatusFault,1); 229 | } 230 | callback(); 231 | }, 232 | 233 | servicePolling: function(){ 234 | var that = this; 235 | this.log.debug('Nut is Polling for %s with polling delay of %s seconds.', this.name, that.platform.nutPolling/1000); 236 | this.getCheck(function (callback) { 237 | setTimeout(function() { 238 | that.servicePolling(); 239 | }, that.platform.nutPolling); 240 | }); 241 | }, 242 | 243 | getServices: function() { 244 | var that = this; 245 | var services = [] 246 | this.service = new Service.ContactSensor(this.name); 247 | this.service.getCharacteristic(Characteristic.ContactSensorState) // Based on ups.status 248 | .on('get', this.getCheck.bind(this));; 249 | this.service.addCharacteristic(Characteristic.StatusActive); // Has load (being used) 250 | this.service.addCharacteristic(Characteristic.StatusFault); // Used if unable to connect to Nut Server 251 | this.service.addCharacteristic(Characteristic.CurrentTemperature); 252 | this.service.addCharacteristic(EnterpriseTypes.InputVoltageAC); 253 | this.service.addCharacteristic(EnterpriseTypes.OutputVoltageAC); 254 | this.service.addCharacteristic(EnterpriseTypes.BatteryVoltageDC); 255 | this.service.addCharacteristic(EnterpriseTypes.UPSLoadPercent); 256 | services.push(this.service); 257 | 258 | this.serviceInfo = new Service.AccessoryInformation(); 259 | this.serviceInfo.setCharacteristic(Characteristic.Manufacturer, this.accVars["device.mfr"] || this.accVars["ups.vendorid"] || 'No Manufacturer') 260 | .setCharacteristic(Characteristic.Name, this.name) 261 | .setCharacteristic(Characteristic.SerialNumber, this.accVars["ups.serial"] || 'No Serial#') 262 | .setCharacteristic(Characteristic.FirmwareRevision, this.accVars["ups.firmware"] || 'No Data') 263 | .setCharacteristic(Characteristic.Model, this.accVars["device.model"].trim() || this.accVars["ups.productid"] || 'No Model#'); 264 | services.push(this.serviceInfo); 265 | 266 | this.serviceBatt = new Service.BatteryService(); 267 | this.serviceBatt.setCharacteristic(Characteristic.BatteryLevel, this.accVars["battery.charge"]) 268 | .setCharacteristic(Characteristic.Name, this.name) 269 | .setCharacteristic(Characteristic.ChargingState, 0) 270 | .setCharacteristic(Characteristic.StatusLowBattery, 0); 271 | services.push(this.serviceBatt); 272 | 273 | return services; 274 | } 275 | } 276 | 277 | module.exports.accessory = NutAccessory; 278 | module.exports.platform = NutPlatform; 279 | 280 | var Service, Characteristic, EnterpriseTypes; 281 | 282 | module.exports = function(homebridge) { 283 | Service = homebridge.hap.Service; 284 | Characteristic = homebridge.hap.Characteristic; 285 | EnterpriseTypes = require('./types.js')(homebridge); 286 | 287 | homebridge.registerAccessory("homebridge-nut-accessory", "NutAccessory", NutAccessory); 288 | homebridge.registerPlatform("homebridge-nut", "Nut", NutPlatform); 289 | }; 290 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-nut", 3 | "version": "1.0.14", 4 | "description": "Homebridge plugin for NUT (Network UPS Tools) Client", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ToddGreenfield/homebridge-nut.git" 12 | }, 13 | "keywords": [ 14 | "homebridge-plugin", 15 | "homebridge", 16 | "nut", 17 | "ups" 18 | ], 19 | "engines": { 20 | "node": ">=0.12.0", 21 | "homebridge": ">=0.2.0" 22 | }, 23 | "author": "Todd Greenfield", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/ToddGreenfield/homebridge-nut/issues" 27 | }, 28 | "homepage": "https://github.com/ToddGreenfield/homebridge-nut#readme", 29 | "dependencies": { 30 | "node-nut": "^1.0.1", 31 | "title-case": "^2.1.1", 32 | "system-sleep": "^1.3.6", 33 | "underscore": "^1.8.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var Nut = require('node-nut'); 2 | 3 | oNut = new Nut(3493, 'localhost'); 4 | 5 | oNut.on('error', function(err) { 6 | console.log('There was an error: ' + err); 7 | }); 8 | 9 | oNut.on('close', function() { 10 | console.log('Connection closed.'); 11 | }); 12 | 13 | oNut.on('ready', function() { 14 | oNut.GetUPSList(function(upslist, err) { 15 | if (err) { 16 | console.log('ERROR ' + err); 17 | } 18 | else { 19 | var ups = Object.keys(upslist); 20 | console.log(upslist); 21 | getVars(ups); 22 | } 23 | }); 24 | }); 25 | 26 | function getVars(ups) { 27 | if (!ups.length) { 28 | oNut.close(); 29 | console.log('DONE'); 30 | return; 31 | } 32 | var currentUps = ups.shift(); 33 | oNut.GetUPSVars(currentUps, function(upsvars, err) { 34 | if (err) { 35 | console.log('ERROR ' + err); 36 | self.close(); 37 | } 38 | else { 39 | console.log('UPS ' + currentUps); 40 | console.log(upsvars); 41 | var obj = Object.keys(upsvars); 42 | for (var prop in obj) { 43 | switch(obj[prop]) { 44 | case "device.mfr": 45 | console.log("Nut eventVar device.mfr received is %s: ", upsvars["device.mfr"]); 46 | break; 47 | case "device.model": 48 | console.log("Nut eventVar device.model received is %s: ", upsvars["device.model"]); 49 | break; 50 | case "ups.productid": 51 | console.log("Nut eventVar ups.productid received is %s: ", upsvars["ups.productid"]); 52 | break; 53 | } 54 | } 55 | getVars(ups); 56 | } 57 | }); 58 | } 59 | 60 | oNut.start(); 61 | -------------------------------------------------------------------------------- /types.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits; 2 | var Service, Characteristic; 3 | 4 | module.exports = function(homebridge) { 5 | Service = homebridge.hap.Service; 6 | Characteristic = homebridge.hap.Characteristic; 7 | UUID = homebridge.hap.uuid; 8 | 9 | var EnterpriseTypes = {}; 10 | 11 | 12 | // Characteristics 13 | 14 | EnterpriseTypes.InputVoltageAC = function() { 15 | var serviceUUID = UUID.generate('EnterpriseTypes:usagedevice:InputVoltageAC'); 16 | Characteristic.call(this, 'Input Voltage AC', serviceUUID); 17 | this.setProps({ 18 | format: Characteristic.Formats.Float, 19 | unit: "V", 20 | minValue: 0, 21 | maxValue: 65535, 22 | minStep: .01, 23 | perms: [ Characteristic.Perms.READ, Characteristic.Perms.NOTIFY ] 24 | }); 25 | this.value = this.getDefaultValue(); 26 | }; 27 | inherits(EnterpriseTypes.InputVoltageAC, Characteristic); 28 | 29 | EnterpriseTypes.OutputVoltageAC = function() { 30 | var serviceUUID = UUID.generate('EnterpriseTypes:usagedevice:OutputVoltageAC'); 31 | Characteristic.call(this, 'Output Voltage AC', serviceUUID); 32 | this.setProps({ 33 | format: Characteristic.Formats.Float, 34 | unit: "V", 35 | minValue: 0, 36 | maxValue: 65535, 37 | minStep: .01, 38 | perms: [ Characteristic.Perms.READ, Characteristic.Perms.NOTIFY ] 39 | }); 40 | this.value = this.getDefaultValue(); 41 | }; 42 | inherits(EnterpriseTypes.OutputVoltageAC, Characteristic); 43 | 44 | EnterpriseTypes.BatteryVoltageDC = function() { 45 | var serviceUUID = UUID.generate('EnterpriseTypes:usagedevice:BatteryVoltageDC'); 46 | Characteristic.call(this, 'Battery Voltage DC', serviceUUID); 47 | this.setProps({ 48 | format: Characteristic.Formats.Float, 49 | unit: "V", 50 | minValue: 0, 51 | maxValue: 65535, 52 | minStep: .01, 53 | perms: [ Characteristic.Perms.READ, Characteristic.Perms.NOTIFY ] 54 | }); 55 | this.value = this.getDefaultValue(); 56 | }; 57 | inherits(EnterpriseTypes.BatteryVoltageDC, Characteristic); 58 | 59 | EnterpriseTypes.UPSLoadPercent = function() { 60 | var serviceUUID = UUID.generate('EnterpriseTypes:usagedevice:UPSLoadPercent'); 61 | Characteristic.call(this, 'UPS Load', serviceUUID); 62 | this.setProps({ 63 | format: Characteristic.Formats.UINT8, 64 | unit: Characteristic.Units.PERCENTAGE, 65 | minValue: 0, 66 | maxValue: 100, 67 | minStep: 1, 68 | perms: [ Characteristic.Perms.READ, Characteristic.Perms.NOTIFY ] 69 | }); 70 | this.value = this.getDefaultValue(); 71 | }; 72 | inherits(EnterpriseTypes.UPSLoadPercent, Characteristic); 73 | 74 | return EnterpriseTypes; 75 | }; --------------------------------------------------------------------------------