iobroker.nut
Version:
Network UPS Adapter
491 lines (444 loc) • 17.2 kB
JavaScript
/**
*
* NUT adapter
*
* Adapter loading NUT data from an UPS
*
*/
/* jshint -W097 */
// jshint strict:true
/*jslint node: true */
/*jslint esversion: 6 */
'use strict';
const utils = require('@iobroker/adapter-core'); // Get common adapter utils
let adapter;
const Nut = require('node-nut');
let nutTimeout;
let stopInProgress;
let nutCommands = null;
let connected = null;
function startAdapter(options) {
options = options || {};
Object.assign(options, {
name: 'nut'
});
adapter = new utils.Adapter(options);
adapter.on('ready', () => {
main();
});
adapter.on('message', (msg) => {
processMessage(msg);
});
adapter.on('stateChange', (id, state) => {
adapter.log.debug('stateChange ' + id + ' ' + JSON.stringify(state));
const realNamespace = adapter.namespace + '.commands.';
const stateId = id.substring(realNamespace.length);
if (!state || state.ack || id.indexOf(realNamespace) !== 0) return;
const command = stateId.replace(/-/g, '.');
initNutConnection((oNut) => {
if (!oNut) {
adapter.log.error('USV not available - Error while sending command: ' + command);
return;
}
if (stopInProgress) return; // adapter already unloaded
if (adapter.config.username && adapter.config.password) {
adapter.log.info('send username for command ' + command);
oNut.SetUsername(adapter.config.username, (err) => {
if (err) {
adapter.log.error('Err while sending username: '+ err);
oNut.close();
}
else {
adapter.log.info('send password for command ' + command);
oNut.SetPassword(adapter.config.password, (err) => {
if (err) {
adapter.log.error('Err while sending password: '+ err);
oNut.close();
}
else {
adapter.log.info('send command ' + command);
oNut.RunUPSCommand(adapter.config.ups_name, command, (err) => {
if (err) {
adapter.log.error('Err while sending command ' + command + ': '+ err);
oNut.close();
}
getCurrentNutValues(oNut, true);
});
}
});
}
});
}
else {
adapter.log.info('send command ' + command + ' without username and password');
oNut.RunUPSCommand(adapter.config.ups_name, command, (err) => {
if (err) {
adapter.log.error('Err while sending command ' + command + ': '+ err);
}
getCurrentNutValues(oNut, true);
});
}
adapter.setState(id, {ack: true, val: false});
});
});
adapter.on('unload', (callback) => {
stopInProgress = true;
if (nutTimeout) clearTimeout(nutTimeout);
nutTimeout = null;
try {
setConnected(false);
} finally {
if (typeof callback === 'function') callback();
}
});
return adapter;
}
function setConnected(isConnected) {
if (connected !== isConnected) {
connected = isConnected;
adapter && adapter.setState('info.connection', connected, true, (err) => {
// analyse if the state could be set (because of permissions)
if (err && adapter && adapter.log) {
adapter.log.error('Can not update connected state: ' + err);
}
else if (adapter && adapter.log) {
adapter.log.debug('connected set to ' + connected);
}
});
}
}
async function main() {
setConnected(false);
adapter.getForeignObject('system.adapter.' + adapter.namespace, (err, obj) => {
if (!err && obj && (obj.common.mode !== 'daemon')) {
obj.common.mode = 'daemon';
if (obj.common.schedule) delete(obj.common.schedule);
adapter.setForeignObject(obj._id, obj);
}
});
adapter.log.debug('Create Channel status');
try {
await adapter.setObjectNotExistsAsync('status', {
type: 'channel',
common: {name: 'status'},
native: {}
});
} catch (err) {
adapter.log.error('Error creating Channel: ' + err);
}
try {
await adapter.setObjectNotExistsAsync('status.severity', {
type: 'state',
common: {
name: 'status.severity',
role: 'indicator',
type: 'number',
read: true,
write: false,
def: 4,
states: {0: 'idle', 1: 'operating', 2: 'operating_critical', 3: 'action_needed', 4: 'unknown'}
},
native: {id: 'status.severity'}
});
} catch (err) {
adapter.log.error('Error creating State: ' + err);
}
await parseAndSetSeverity("", true);
try {
await adapter.setObjectNotExistsAsync('status.last_notify', {
type: 'state',
common: {
name: 'status.last_notify',
type: 'string',
role: 'state',
read: true,
write: false
},
native: {
id: 'status.last_notify'
}
});
} catch (err) {
adapter.log.error('Error creating State: ' + err);
}
adapter.getState('status.last_notify', async (err, state) => {
if (!err && !state) {
await adapter.setStateAsync('status.last_notify', {ack: true, val: ''});
}
initializeNutDevice();
});
}
function initializeNutDevice() {
const update_interval = parseInt(adapter.config.update_interval,10) || 60;
initNutConnection((oNut) => {
if (!oNut) {
adapter.log.error('USV not available - Delay initialization');
nutTimeout = setTimeout(initializeNutDevice, update_interval * 1000);
return;
}
oNut.GetUPSCommands(adapter.config.ups_name, (cmdlist, err) => {
if (err) {
adapter.log.error('Err while getting all commands: '+ err);
}
else {
adapter.log.debug('Got commands, create and subscribe command states');
initNutCommands(cmdlist);
}
getCurrentNutValues(oNut, true);
nutTimeout = setTimeout(updateNutData, update_interval * 1000);
});
});
}
async function initNutCommands(cmdlist) {
adapter.log.debug('Create Channel commands');
try {
await adapter.setObjectNotExistsAsync('commands', {
type: 'channel',
common: {name: 'commands'},
native: {}
});
} catch (err) {
adapter.log.error('Error creating Channel: ' + err);
}
if (! cmdlist) return;
nutCommands = cmdlist;
for (let i = 0; i < cmdlist.length; i++) {
const cmdName = cmdlist[i].replace(/\./g, '-');
adapter.log.debug('Create State commands.' + cmdName);
try {
await adapter.setObjectNotExistsAsync('commands.' + cmdName, {
type: 'state',
common: {
name: 'commands.' + cmdName,
role: 'button',
type: 'boolean',
read: true,
write: true,
def: false
},
native: {id: 'commands.' + cmdName}
});
} catch (err) {
adapter.log.error('Error creating State: ' + err);
}
await adapter.setStateAsync('commands.' + cmdName, {ack: true, val: false});
}
adapter.subscribeStates('commands.*');
}
/*
Command Datapoint to be used with "NOIFY EVENTS" and upsmon
ONLINE : The UPS is back on line.
ONBATT : The UPS is on battery.
LOWBATT : The UPS battery is low (as determined by the driver).
FSD : The UPS has been commanded into the "forced shutdown" mode.
COMMOK : Communication with the UPS has been established.
COMMBAD : Communication with the UPS was just lost.
SHUTDOWN : The local system is being shut down.
REPLBATT : The UPS needs to have its battery replaced.
NOCOMM : The UPS can’t be contacted for monitoring.
*/
function processMessage(message) {
if (!message) return;
adapter.log.info('Message received = ' + JSON.stringify(message));
let updateNut = false;
if (message.command === 'notify' && message.message) {
adapter.log.info('got Notify ' + message.message.notifytype + ' for: ' + message.message.upsname);
const ownName = adapter.config.ups_name + '@' + adapter.config.host_ip;
adapter.log.info('ownName=' + ownName + ' --> ' + (ownName === message.message.upsname));
if (ownName === message.message.upsname) {
updateNut = true;
adapter.setState('status.last_notify', {ack: true, val: message.message.notifytype});
if (message.message.notifytype==='COMMBAD' || message.message.notifytype==='NOCOMM') {
parseAndSetSeverity("OFF");
}
}
}
else updateNut = true;
if (updateNut) {
if (nutTimeout) clearTimeout(nutTimeout);
updateNutData();
}
}
function initNutConnection(callback) {
adapter.config.host_port = parseInt(adapter.config.host_port);
if ((adapter.config.host_port < 0 || adapter.config.host_port > 65535) || isNaN(adapter.config.host_port)) {
adapter.log.error('Configured Port invalid: ' + adapter.config.host_port);
adapter.terminate ? adapter.terminate(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) : process.exit(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
return;
}
const oNut = new Nut(adapter.config.host_port, adapter.config.host_ip);
oNut.on('error', (err) => {
if (stopInProgress) return; // adapter already unloaded
adapter.log.error('Error happened: ' + err);
setConnected(false);
adapter.getState('status.last_notify', (err, state) => {
if (!err && !state || (state && state.val!=='COMMBAD' && state.val!=='SHUTDOWN' && state.val!=='NOCOMM')) {
adapter.setState('status.last_notify', {ack: true, val: 'ERROR'});
}
if (!err) parseAndSetSeverity("");
callback(null);
});
});
oNut.on('close', () => {
adapter.log.debug('NUT Connection closed. Done.');
});
oNut.on('ready', () => {
adapter.log.debug('NUT Connection ready');
setConnected(true);
callback(oNut);
});
oNut.start();
}
function updateNutData() {
adapter.log.debug('Start NUT update');
initNutConnection((oNut) => {
oNut && getCurrentNutValues(oNut, true);
});
const update_interval = parseInt(adapter.config.update_interval, 10) || 60;
nutTimeout = setTimeout(updateNutData, update_interval * 1000);
}
function getCurrentNutValues(oNut, closeConnection) {
oNut.GetUPSVars(adapter.config.ups_name, (varlist, err) => {
if (err) {
adapter.log.error('Err while getting NUT values: '+ err);
}
else {
adapter.log.debug('Got values, start setting them');
storeNutData(varlist);
}
if (closeConnection) oNut.close();
});
}
async function storeNutData(varlist) {
let last = '';
let current = '';
let index = 0;
let stateName = '';
for (let key in varlist) {
if (!varlist.hasOwnProperty(key)) continue;
index = key.indexOf('.');
if (index > 0) {
current = key.substring(0, index);
}
else {
current = '';
last = '';
index = -1;
}
if (((last === '') || (last !== current)) && (current !== '')) {
adapter.log.debug('Create Channel ' + current);
try {
await adapter.setObjectNotExistsAsync(current, {
type: 'channel',
common: {
name: current
},
native: {}
});
} catch (err) {
adapter.log.error(`Error creating Channel: ${err}`);
}
}
stateName = current + '.' + key.substring(index + 1).replace(/\./g,'-');
adapter.log.debug(`Create State ${stateName}`);
if (stateName === 'battery.charge') {
try {
// it has type string but should be an integer
varlist[key] = parseInt(varlist[key])
await adapter.setObjectNotExistsAsync(stateName, {
type: 'state',
common: {name: stateName, type: 'number', role: 'value.battery', read: true, write: false, unit: '%'},
native: {id: stateName}
});
} catch (err) {
adapter.log.error('Error creating State: ' + err);
}
}
else {
try {
await adapter.setObjectNotExistsAsync(stateName, {
type: 'state',
common: {name: stateName, type: 'string', read: true, write: false},
native: {id: stateName}
});
} catch (err) {
adapter.log.error('Error creating State: ' + err);
}
}
adapter.log.debug(`Set State ${stateName} = ${varlist[key]}`);
await adapter.setStateAsync(stateName, {ack: true, val: varlist[key]});
last = current;
}
if (varlist['ups.status']) {
await parseAndSetSeverity(varlist['ups.status']);
}
else {
await parseAndSetSeverity("");
}
adapter.log.debug('All Nut values set');
}
async function parseAndSetSeverity(ups_status, createObjects) {
const statusMap = {
'OL': {name: 'online', severity: 'idle'},
'OB': {name: 'onbattery', severity: 'operating'},
'LB': {name: 'lowbattery', severity: 'operating_critical'},
'HB': {name: 'highbattery', severity: 'operating_critical'},
'RB': {name: 'replacebattery', severity: 'action_needed'},
'CHRG': {name: 'charging', severity: 'idle'},
'DISCHRG': {name: 'discharging', severity: 'operating'},
'BYPASS': {name: 'bypass', severity: 'action_needed'},
'CAL': {name: 'calibration', severity: 'operating'},
'OFF': {name: 'offline', severity: 'action_needed'},
'OVER': {name: 'overload', severity: 'action_needed'},
'TRIM': {name: 'trimming', severity: 'operating'},
'BOOST': {name: 'boosting', severity: 'operating'},
'FSD': {name: 'shutdown', severity: 'operating_critical'}
};
const severity = {
'idle': false,
'operating': false,
'operating_critical': false,
'action_needed': false
};
if (ups_status.indexOf('FSD') !== -1) {
ups_status += ' OB LB';
}
const checker = ' ' + ups_status + ' ';
let stateName = "";
for (var idx in statusMap) {
if (statusMap.hasOwnProperty(idx)) {
const found = (checker.indexOf(' ' + idx) > -1);
stateName = 'status.' + statusMap[idx].name;
adapter.log.debug('Create State ' + stateName);
try {
createObjects && await adapter.setObjectNotExistsAsync(stateName, {
type: 'state',
common: {name: stateName, type: 'boolean', read: true, write: false},
native: {id: stateName}
});
} catch (err) {
adapter.log.error('Error creating State: ' + err);
}
adapter.log.debug('Set State '+stateName+' = '+found);
await adapter.setStateAsync(stateName, {ack: true, val: found});
if (found) {
severity[statusMap[idx].severity] = true;
adapter.log.debug('Severity Flag ' + statusMap[idx].severity + '=true');
}
}
}
let severityVal = 4;
if (severity.operating_critical) severityVal = 2;
else if (severity.action_needed) severityVal = 3;
else if (severity.operating) severityVal = 1;
else if (severity.idle) severityVal = 0;
adapter.log.debug('Set State status.severity = ' + severityVal);
await adapter.setStateAsync('status.severity', {ack: true, val: severityVal});
}
// If started as allInOne/compact mode => return function to create instance
if (module && module.parent) {
module.exports = startAdapter;
} else {
// or start the instance directly
startAdapter();
}