iobroker.nut
Version:
Network UPS Adapter
524 lines (477 loc) • 17.8 kB
JavaScript
/**
*
* NUT adapter
*
* Adapter loading NUT data from an UPS
*
*/
/* jshint -W097 */
// jshint strict:true
/*jslint node: true */
/*jslint esversion: 6 */
;
const utils = require('@iobroker/adapter-core'); // Get common adapter utils
let adapter;
const Nut = require('node-nut');
let nutTimeout;
let stopInProgress;
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;
}
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 (!Object.prototype.hasOwnProperty.call(varlist, 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 (Object.prototype.hasOwnProperty.call(statusMap, 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();
}