'use strict';
const _isNil = require('lodash/isNil');
const _has = require('lodash/has');
const _isEmpty = require('lodash/isEmpty');
const _includes = require('lodash/includes');
const events = require('events');
const {default: PromiseQueue} = require('p-queue');
const Chance = require('chance');
const debug = require('debug')('k4-base-bridge:nodeStateMonitor');
let TimingUtility;
let MappingChangeMonitor;
let ModelChangeMonitor;
/**
* @class
* @description Plugin to observe and update node states
*/
class NodeStateMonitor {
/**
* @constructor
* @param {Object} [opts] General settings for NodeStateMonitor
* @param {Adapter} adapter Adapter for which this is a plugin
*/
constructor(opts, adapter) {
this._opts = opts;
this._adapter = adapter;
this.emitter = new events.EventEmitter();
this._initCloseQueue = new PromiseQueue({
concurrency: 1,
autoStart: true
});
this._shouldReinitializeOnBridgeReady = false;
if (this._opts &&
this._opts.shouldReinitializeOnBridgeReady === true) {
this._shouldReinitializeOnBridgeReady = true;
}
TimingUtility = this._adapter.bridge.classes.TimingUtility;
MappingChangeMonitor = this._adapter.bridge.classes.MappingChangeMonitor;
ModelChangeMonitor = this._adapter.bridge.classes.ModelChangeMonitor;
this.mappingChangeMonitor = new MappingChangeMonitor(adapter);
this.modelChangeMonitor = new ModelChangeMonitor(adapter);
}
get shouldReinitializeOnBridgeReady() {
return this._shouldReinitializeOnBridgeReady;
}
/**
* @description The adapter instance
* @type {Adapter}
*/
set adapter(adapter) {
this._adapter = adapter;
}
get adapter() {
return this._adapter;
}
/**
* @description The options object for this NodeStateMonitor
* @type {Object}
*/
get opts() {
return this._opts;
}
set opts(opts) {
this._opts = opts;
}
/**
* @property {Object} constants
* @static
*/
static get constants() {
return {
resetModes: {
STANDARD_DEAD: 'nodeDeadTimeout',
FAILURE_RECOVERY: 'failureRecoveryTimeout'
},
nodeStates: {
ALIVE: 'alive',
DEAD: 'dead',
UNKNOWN: 'unknown'
},
nodeStatusTypes: {
DEAD: 0,
ALIVE: 1,
NOT_RESPONDING: 2
},
emittedEventTypes: {
PING_READY: 'readyToPing'
},
misc: {
MAX_RECOVERY_ATTEMPTS: 1
}
};
}
/**
* @description Obtains the latest nodeState from the device model node
* @param {String} devicePath The path to the device model node
* @throws {Error}
*/
getNodeStateByDevicePath(devicePath) {
const deviceNode = this.adapter.k4.model.child(devicePath);
if (_isNil(deviceNode)) {
throw new Error(`Device node does not exist for: ${devicePath}`);
} else if (_isNil(deviceNode.child('variables/nodeState'))) {
throw new Error(`nodeState variable does not exist for: ${devicePath}`);
} else {
return deviceNode.child('variables/nodeState').value();
}
}
/**
* @description Retrieves the current device node state for the specified
* node id
* @param {Number} nodeId
* @throws {Error}
*/
getNodeStateById(nodeId) {
const devicePath = this.findDevicePathById(nodeId);
const nodeState = this.getNodeStateByDevicePath(devicePath);
return nodeState;
}
/**
* @description This method should be named as 'init' because Adapter
* instance calls the 'init' methods (if they exist) for all Adapter
* plugins
*/
init(options) {
debug('[init] Starting Node State Monitor: ', options);
this.havePolledThisWindow = false;
if (!_isNil(options) && !_isNil(options.force) && options.force === true) {
this._initCloseQueue.pause();
this._initCloseQueue.clear();
this._initCloseQueue.start();
}
return this._initCloseQueue.add(() => this.initRoutine());
}
initRoutine() {
this._enableEventEmissions = false;
return TimingUtility.setTimeoutPromise(1)
.then(() => this.closeRoutine())
.then(() => {
this._asyncCriticalTaskQueue = new PromiseQueue({
concurrency: 1,
autoStart: true
});
this._rng = new Chance((process.hrtime()[0] * 1e9) + process.hrtime()[1]);
this.pingTimersByDeviceNode = new Map();
this.lastSeenTimersByDeviceNode = new Map();
this.failureRecoveryAttemptsByDeviceNode = new Map();
const startupPromise = this._asyncCriticalTaskQueue.add(() => {
this.initializeFailureRecoveryAttemptCounts();
this.initializeNodeStatesForAllDevices();
this.setupNodeStatusAdapterEventListener();
this.initializePingTimers();
const initialPingPromises = this.initializeLastSeenTimers();
return initialPingPromises
.then(() => TimingUtility.setTimeoutPromise(2000))
.then(() => {
this._enableEventEmissions = true;
});
});
this.mappingChangeMonitor.init();
this.modelChangeMonitor.init();
this.mappingChangeHandler = (devicesWithChangedMappings) => {
this._asyncCriticalTaskQueue.add(() => {
return TimingUtility.setTimeoutPromise(200)
.then(() => {
return this.onMappingChanged(devicesWithChangedMappings);
});
});
};
this.modelChangeHandler = (devicesWithChangedNodes) => {
this._asyncCriticalTaskQueue.add(() => {
return TimingUtility.setTimeoutPromise(200)
.then(() => {
return this.onModelChanged(devicesWithChangedNodes);
});
});
};
return startupPromise
.then(() => {
this.mappingChangeMonitor.emitter.on(MappingChangeMonitor.constants.MAPPING_CHANGED_EVENT_NAME, this.mappingChangeHandler);
this.modelChangeMonitor.emitter.on(ModelChangeMonitor.constants.MODEL_NODES_CHANGED_EVENT_NAME, this.modelChangeHandler);
});
});
}
initializeNodeStatesForAllDevices() {
debug('[initializeNodeStatesForAllDevices] Going to set initial node states for all devices');
const bridgeDevicesList = this.adapter.getBridgeDevicesList();
Object.keys(bridgeDevicesList)
.forEach((devicePath) => {
if (bridgeDevicesList[devicePath].isTemporaryDevice === true) {
return;
}
const deviceModelNode = this.adapter.k4.model.child(devicePath);
if (_isNil(deviceModelNode)) {
return;
}
const deviceCls = deviceModelNode.cls();
// NOTE: Do not want to monitor node states for the bridge itself
if (deviceCls.includes('bridge')) {
return;
}
this.updateNodeState(devicePath, NodeStateMonitor.constants.nodeStates.UNKNOWN);
}, this);
debug('[initializeNodeStatesForAllDevices] Finished settinginitial node states for all devices');
}
initializeFailureRecoveryAttemptCounts() {
const bridgeDevicesList = this.adapter.getBridgeDevicesList();
Object.keys(bridgeDevicesList)
.forEach((devicePath) => {
if (bridgeDevicesList[devicePath].isTemporaryDevice === true) {
return;
}
const deviceModelNode = this.adapter.k4.model.child(devicePath);
if (_isNil(deviceModelNode)) {
return;
}
const deviceCls = deviceModelNode.cls();
// NOTE: Do not want to monitor node states for the bridge itself
if (deviceCls.includes('bridge')) {
return;
}
this.failureRecoveryAttemptsByDeviceNode.set(devicePath, 0);
}, this);
}
onMappingChanged(devicesWithChangedMappings) {
debug('[onMappingChanged] Entered model change handler: ', devicesWithChangedMappings.length, new Date().toISOString());
devicesWithChangedMappings
.forEach((deviceChangeObj) => {
try {
switch (deviceChangeObj.changeType) {
case MappingChangeMonitor.constants.DEVICE_ADDED: {
this.adapter.k4.logging.system(`[NodeStateMonitor] Device mapping was added: ${deviceChangeObj.devicePath}`);
const deviceModelNode = this.adapter.k4.model.child(deviceChangeObj.devicePath);
if (_isNil(deviceModelNode)) {
return;
}
const deviceCls = deviceModelNode.cls();
if (deviceCls.includes('bridge')) {
return;
}
// NOTE: Initializing nodeState to UNKNOWN at inception
this.updateNodeState(deviceChangeObj.devicePath, NodeStateMonitor.constants.nodeStates.UNKNOWN);
// NOTE: Set up timers for this new device
const device = this.adapter.findBridgeDeviceByNode(deviceChangeObj.devicePath);
if (_has(device, '_mapping')) {
const nodeMonitoringSettings = this.extractNodeMonitoringSettings(device);
this.resetLastSeenTimerForDevice(deviceChangeObj.devicePath, NodeStateMonitor.constants.resetModes.STANDARD_DEAD, nodeMonitoringSettings);
// FIXME: Guarantee at least one ping on change
if (this._enableEventEmissions) {
this.emitter.emit('mappingDeviceAddedPing', deviceChangeObj.devicePath, Date.now());
}
this.issuePing(deviceChangeObj.devicePath);
this.resetPingTimerForDevice(deviceChangeObj.devicePath, nodeMonitoringSettings);
}
} break;
case MappingChangeMonitor.constants.DEVICE_MODIFIED_IN_PLACE: {
this.adapter.k4.logging.system(`[NodeStateMonitor] Device mapping was modified in place: ${deviceChangeObj.devicePath}. Fields changed: ${deviceChangeObj.fieldsThatChanged}`);
const deviceModelNode = this.adapter.k4.model.child(deviceChangeObj.devicePath);
if (_isNil(deviceModelNode)) {
return;
}
const deviceCls = deviceModelNode.cls();
if (deviceCls.includes('bridge')) {
return;
}
const device = this.adapter.findBridgeDeviceByNode(deviceChangeObj.devicePath);
if (_has(device, '_mapping')) {
const nodeMonitoringSettings = this.extractNodeMonitoringSettings(device);
this.resetLastSeenTimerForDevice(deviceChangeObj.devicePath, NodeStateMonitor.constants.resetModes.STANDARD_DEAD, nodeMonitoringSettings);
this.removePingTimerForDevice(deviceChangeObj.devicePath);
// FIXME: Guarantee at least one ping on change
if (this._enableEventEmissions) {
this.emitter.emit('mappingDeviceChangedPing', deviceChangeObj.devicePath, Date.now());
}
this.issuePing(deviceChangeObj.devicePath);
this.resetPingTimerForDevice(deviceChangeObj.devicePath, nodeMonitoringSettings);
}
} break;
case MappingChangeMonitor.constants.DEVICE_REMOVED:
this.adapter.k4.logging.system(`[NodeStateMonitor] Device mapping was removed: ${deviceChangeObj.devicePath}`);
const deviceModelNode = this.adapter.k4.model.child(deviceChangeObj.devicePath);
if (!_isNil(deviceModelNode)) {
const deviceCls = deviceModelNode.cls();
if (deviceCls.includes('bridge')) {
return;
}
}
// NOTE: Remove timers for this device
this.removeLastSeenTimerForDevice(deviceChangeObj.devicePath);
this.removePingTimerForDevice(deviceChangeObj.devicePath);
break;
default:
break;
}
} catch (e) {
this.adapter.k4.logging.error(`There was an issue handling an adapter mapping change. ${e.toString()}`, e instanceof Error ? e.stack.split('\n') : null);
}
}, this);
}
onModelChanged(devicesWithChangedNodes) {
debug('[onModelChanged] Entered model change handler: ', devicesWithChangedNodes.length, new Date().toISOString());
devicesWithChangedNodes
.forEach((deviceNodeChangeObj) => {
this.adapter.k4.logging.debug(`[nodeStateMonitor]/onModelChanged -- Device Node Changed Path: ${deviceNodeChangeObj.devicePath}.`)
if (!this.adapter.k4.model.child(deviceNodeChangeObj.devicePath)) {
this.adapter.k4.logging.debug(`[nodeStateMonitor]/onModelChanged -- Device Node Changed Path: ${deviceNodeChangeObj.devicePath}. Does not exist on the K4Model`);
}
try {
switch (deviceNodeChangeObj.changeType) {
case ModelChangeMonitor.constants.DEVICE_ADDED: {
this.adapter.k4.logging.system(`[NodeStateMonitor] Device node was added: ${deviceNodeChangeObj.devicePath}`);
const deviceModelNode = this.adapter.k4.model.child(deviceNodeChangeObj.devicePath);
if (_isNil(deviceModelNode)) {
return;
}
const deviceCls = deviceModelNode.cls();
// NOTE: Do not want to monitor node states for the bridge itself
if (deviceCls.includes('bridge')) {
return;
}
// NOTE: Initializing nodeState to UNKNOWN at inception
this.updateNodeState(deviceNodeChangeObj.devicePath, NodeStateMonitor.constants.nodeStates.UNKNOWN);
// NOTE: Set up timers for this new device
const device = this.adapter.findBridgeDeviceByNode(deviceNodeChangeObj.devicePath);
if (_has(device, '_mapping')) {
const nodeMonitoringSettings = this.extractNodeMonitoringSettings(device);
this.resetLastSeenTimerForDevice(deviceNodeChangeObj.devicePath, NodeStateMonitor.constants.resetModes.STANDARD_DEAD, nodeMonitoringSettings);
// FIXME: Guarantee at least one ping on change
if (this._enableEventEmissions) {
this.emitter.emit('deviceNodeAddedPing', deviceNodeChangeObj.devicePath, Date.now());
}
this.issuePing(deviceNodeChangeObj.devicePath);
this.resetPingTimerForDevice(deviceNodeChangeObj.devicePath, nodeMonitoringSettings);
}
} break;
case ModelChangeMonitor.constants.DEVICE_MODIFIED_IN_PLACE: {
this.adapter.k4.logging.system(`[NodeStateMonitor] Device node was modified in place: ${deviceNodeChangeObj.devicePath}. Fields changed: ${deviceNodeChangeObj.fieldsThatChanged}`);
const deviceModelNode = this.adapter.k4.model.child(deviceNodeChangeObj.devicePath);
if (_isNil(deviceModelNode)) {
return;
}
const deviceCls = deviceModelNode.cls();
// NOTE: Do not want to monitor node states for the bridge itself
if (deviceCls.includes('bridge')) {
return;
}
const device = this.adapter.findBridgeDeviceByNode(deviceNodeChangeObj.devicePath);
if (!_isNil(deviceNodeChangeObj.fieldsThatChanged.find(modelField => modelField === 'properties'))) {
if (_has(device, '_mapping')) {
const nodeMonitoringSettings = this.extractNodeMonitoringSettings(device);
this.resetLastSeenTimerForDevice(deviceNodeChangeObj.devicePath, NodeStateMonitor.constants.resetModes.STANDARD_DEAD, nodeMonitoringSettings);
this.removePingTimerForDevice(deviceNodeChangeObj.devicePath);
// FIXME: Guarantee at least one ping on change
if (this._enableEventEmissions) {
this.emitter.emit('deviceNodeChangedPing', deviceNodeChangeObj.devicePath, Date.now());
}
this.issuePing(deviceNodeChangeObj.devicePath);
this.resetPingTimerForDevice(deviceNodeChangeObj.devicePath, nodeMonitoringSettings);
}
}
} break;
case ModelChangeMonitor.constants.DEVICE_REMOVED:
this.adapter.k4.logging.system(`[NodeStateMonitor] Device node was removed: ${deviceNodeChangeObj.devicePath}`);
const deviceModelNode = this.adapter.k4.model.child(deviceNodeChangeObj.devicePath);
if (!_isNil(deviceModelNode)) {
const deviceCls = deviceModelNode.cls();
// NOTE: Do not want to monitor node states for the bridge itself
if (deviceCls.includes('bridge')) {
return;
}
}
// NOTE: Remove timers for this device
this.removeLastSeenTimerForDevice(deviceNodeChangeObj.devicePath);
this.removePingTimerForDevice(deviceNodeChangeObj.devicePath);
break;
default:
break;
}
} catch (e) {
this.adapter.k4.logging.error(`[NodeStateMonitor] There was an issue handling an device nodes change. ${e.toString()}`, e instanceof Error ? e.stack.split('\n') : null);
}
}, this);
}
/**
* @description Creates an EventListener to monitor updates of node statuses
* emitted by the Adapter instance
*/
setupNodeStatusAdapterEventListener() {
if (!_isNil(this.adapter._emitter)) {
this.nodeStatusEventListener = (nodeId, nodeStatus) => {
this._asyncCriticalTaskQueue.add(() => {
return TimingUtility.setTimeoutPromise(200)
.then(() => {
return this.onNodeStatusEvent(nodeId, nodeStatus);
});
});
};
this.adapter._emitter.on('nodeStatusUpdate', this.nodeStatusEventListener);
}
}
/**
* @description Sends a noOp command to the device
* @param {String} devicePath Path to the device model node
*/
issuePing(devicePath) {
debug(`[issuePing] Entered for device ${devicePath}: `, new Date().toISOString());
const deviceModelNode = this.adapter.k4.model.child(devicePath);
if (_isNil(deviceModelNode)) {
return;
}
const deviceCls = deviceModelNode.cls();
// NOTE: Do not want to monitor node states for the bridge itself
if (deviceCls.includes('bridge')) {
return;
}
const device = this.adapter.findBridgeDeviceByNode(devicePath);
if (this._enableEventEmissions) {
this.emitter.emit('readyToPing', devicePath, Date.now());
}
debug(`[issuePing] Issuing NoOp ping for device ${devicePath}: `, new Date().toISOString());
this.adapter.sendCommand('NoOp', device, { ackRequested: true })
.catch(err => this.adapter.k4.logging.system(`Had difficulty issuing ping for device: ${devicePath}. ${err.toString()}`, err instanceof Error ? err.stack.split('\n') : null));
}
/**
* @description Obtain the set of node (state) monitoring settings from
* mapping
* @param {Device} device The device whose nodeState is monitored
* @throws {Error}
* @returns {Object} The settings for monitoring nodeState for this device
*/
extractNodeMonitoringSettings(device) {
let deviceNodeMonitoringSettings = {};
if (!_isNil(device) &&
!_isNil(device._mapping) &&
!_isNil(device._mapping.nodeMonitoring)) {
deviceNodeMonitoringSettings = device._mapping.nodeMonitoring;
}
if (_isEmpty(deviceNodeMonitoringSettings)) {
throw new Error(`Unable to access device ${device.devicePath} mapping for node monitoring`);
}
return deviceNodeMonitoringSettings;
}
/**
* @description Determine the value to set for a specific node monitoring
* setting argument, giving primacy to K4Model property overrides
* @param {String} argName Name of the argument belonging to a node monitoring
* parameter
* @param {Device} device The device whose nodeState is monitored
* @param {Object} nodeMonitoringSettings The settings for monitoring
* @returns {any} The appropriate value the argument should be set to
*/
obtainNodeMonitorSettingArgVal(argName, device, nodeMonitoringSettings) {
if (device.isTemporaryDevice) {
return nodeMonitoringSettings[argName];
}
const modelOverrideVal = this.obtainNodeMonitorPropOverrideForArgVal(argName, device, nodeMonitoringSettings);
if (!_isNil(modelOverrideVal)) {
return modelOverrideVal;
}
// Return default argument value if there are no model property
// overrides
return nodeMonitoringSettings[argName];
}
/**
* @description Determine, if available, the K4Model property override value
* for a node monitoring argument overrides
* @param {String} argName Name of the argument belonging to a node monitoring
* parameter
* @param {Device} device The device whose nodeState is monitored
* @param {Object} nodeMonitoringSettings The settings for monitoring
* @returns {any} The appropriate value the argument should be set to
*/
obtainNodeMonitorPropOverrideForArgVal(argName, device, nodeMonitoringSettings) {
let propOverrides = null;
if (!_isNil(nodeMonitoringSettings) && !_isNil(nodeMonitoringSettings.property)) {
propOverrides = nodeMonitoringSettings.property
.find(prop => prop.argument === argName, this);
}
if (!_isNil(propOverrides)) {
if (!device.model.child(`properties/${propOverrides.name}`)) {
this.adapter.k4.logging.system(`K4Model property node to override with does not exist: ${propOverrides.name}`);
return undefined;
}
const propOverrideValue = device.model.child(`properties/${propOverrides.name}`).value();
return propOverrideValue;
}
}
/**
* @description Ensure that lastSeenTimers (i.e. last-heard-from-device
* timers) are set up properly.
* @returns {Promise}
*/
initializeLastSeenTimers() {
const initialPingsIssuedPromises = [];
const bridgeDevicesList = this.adapter.getBridgeDevicesList();
Object.keys(bridgeDevicesList)
.forEach((devicePath) => {
if (bridgeDevicesList[devicePath].isTemporaryDevice === true) {
return;
}
const deviceModelNode = this.adapter.k4.model.child(devicePath);
if (_isNil(deviceModelNode)) {
return;
}
const deviceCls = this.adapter.k4.model.child(devicePath).cls();
// NOTE: Do not want to monitor node states for the bridge itself
if (deviceCls.includes('bridge')) {
return;
}
try {
const nodeMonitoringSettings = this.extractNodeMonitoringSettings(this.adapter.findBridgeDeviceByNode(devicePath));
this.resetLastSeenTimerForDevice(devicePath, NodeStateMonitor.constants.resetModes.STANDARD_DEAD, nodeMonitoringSettings);
// FIXME: Guarantee at least one ping on change
initialPingsIssuedPromises.push(this.issuePing(devicePath));
} catch (e) {
this.adapter.k4.logging.info(`Could not set up lastSeen timer. Device ${devicePath} does not have node monitoring settings. ${e.toString()}`, e instanceof Error ? e.stack.split('\n') : null);
}
}, this);
if (initialPingsIssuedPromises.length === 0) {
return Promise.resolve([]);
}
return Promise.all(initialPingsIssuedPromises);
}
initializePingTimers() {
let disableAutoPingsGlobally = false;
if (!_isNil(this._opts) &&
!_isNil(this._opts.disableAutoPingsGlobally)) {
disableAutoPingsGlobally = this._opts.disableAutoPingsGlobally;
}
if (disableAutoPingsGlobally === true) {
return;
}
const bridgeDevicesList = this.adapter.getBridgeDevicesList();
Object.keys(bridgeDevicesList)
.forEach((devicePath) => {
if (bridgeDevicesList[devicePath].isTemporaryDevice === true) {
return;
}
const deviceModelNode = this.adapter.k4.model.child(devicePath);
if (_isNil(deviceModelNode)) {
return;
}
const deviceCls = deviceModelNode.cls();
// NOTE: Do not want to monitor node states for the bridge itself
if (deviceCls.includes('bridge')) {
return;
}
try {
const nodeMonitoringSettings = this.extractNodeMonitoringSettings(this.adapter.findBridgeDeviceByNode(devicePath));
let enableAutoPing = true;
if (!_isNil(nodeMonitoringSettings) &&
!_isNil(nodeMonitoringSettings.enableAutoPing)) {
enableAutoPing = nodeMonitoringSettings.enableAutoPing;
}
if (enableAutoPing === false) {
this.adapter.k4.logging.debug(`[NodeStateMonitor] Auto pings were disabled for ${devicePath}. Skipping ping timer initialization.`);
return;
}
this.resetPingTimerForDevice(devicePath, nodeMonitoringSettings);
} catch (e) {
this.adapter.k4.logging.info(`Could not set up ping timer. Device ${devicePath} does not have node monitoring settings. ${e.toString()}`, e instanceof Error ? e.stack.split('\n') : null);
}
}, this);
}
// FIXME: Want to account for bridge devices list changes
prunePingTimers() {
const bridgeDevicesListPaths = Object.keys(this.adapter.getBridgeDevicesList());
this.pingTimersByDeviceNode
.forEach((pingTimer, devicePath) => {
if (!_includes(bridgeDevicesListPaths, devicePath)) {
this.removePingTimerForDevice(devicePath);
}
});
}
pruneLastSeenTimers() {
const bridgeDevicesListPaths = Object.keys(this.adapter.getBridgeDevicesList());
this.lastSeenTimersByDeviceNode
.forEach((lastSeenTimer, devicePath) => {
if (!_includes(bridgeDevicesListPaths, devicePath)) {
this.removeLastSeenTimerForDevice(devicePath);
}
});
}
/**
* @description Updates the K4Model variable for the specified
* device's node state
* @param {String} devicePath Path to the device model node
* @param {String} nodeState The raw, local node state for this device
*/
updateNodeState(devicePath, nodeState) {
debug(`[updateNodeState] Node State updated to ${nodeState} for device ${devicePath}`);
const deviceModelNode = this.adapter.k4.model.child(devicePath);
if (_isNil(deviceModelNode)) {
return;
}
const deviceCls = deviceModelNode.cls();
// NOTE: Do not want to monitor node states for the bridge itself
if (deviceCls.includes('bridge')) {
return Promise.resolve();
}
const nodeId = (!_isNil(this.adapter.k4.model.child(`${devicePath}/properties/networkId`))) ? this.adapter.k4.model.child(`${devicePath}/properties/networkId`).value() : null;
// NOTE: Disabled event emission for nodeState UNKNOWN so that tests
// do not break without actually failing.
if (nodeState !== NodeStateMonitor.constants.nodeStates.UNKNOWN) {
if (this._enableEventEmissions) {
this.emitter.emit('nodeStateUpdate', devicePath, nodeState, Date.now());
}
}
this.adapter.k4.model.child(`${devicePath}/variables/nodeState`).set(nodeState);
}
/**
* @description Unregisters listener from
* node status update events emitted by the Adapter instance
*/
removeNodeStatusAdapterEventListener() {
if (!_isNil(this.nodeStatusEventListener) && !_isNil(this.adapter._emitter)) {
this.adapter._emitter.removeListener('nodeStatusUpdate', this.nodeStatusEventListener);
this.nodeStatusEventListener = null;
}
}
/**
* @description Handles the updating of node states and timer
* renewals
* @param {Number} nodeId The nodeId of the device whose node status was
* updated
* @param {any} nodeStatus The emitted status for this node
*/
onNodeStatusEvent(nodeId, nodeStatus) {
try {
const devicePath = this.findDevicePathById(nodeId);
const deviceModelNode = this.adapter.k4.model.child(devicePath);
debug(`[onNodeStatusEvent] Got node status: ${nodeStatus} for ${devicePath}`);
if (_isNil(deviceModelNode)) {
throw new Error('K4Model node does not exist');
}
const deviceCls = deviceModelNode.cls();
// NOTE: Do not want to monitor node states for the bridge itself
if (deviceCls.includes('bridge')) {
return;
}
const device = this.adapter.findBridgeDeviceByNode(devicePath);
switch (nodeStatus) {
case NodeStateMonitor.constants.nodeStatusTypes.ALIVE: {
this.updateNodeState(devicePath, NodeStateMonitor.constants.nodeStates.ALIVE);
const nodeMonitoringSettings = this.extractNodeMonitoringSettings(device);
this.resetLastSeenTimerForDevice(devicePath, NodeStateMonitor.constants.resetModes.STANDARD_DEAD, nodeMonitoringSettings);
this.resetPingTimerForDevice(devicePath, nodeMonitoringSettings);
} break;
case NodeStateMonitor.constants.nodeStatusTypes.NOT_RESPONDING: {
const nodeMonitoringSettings = this.extractNodeMonitoringSettings(device);
const timeoutDurationForFailureRecovery = this.obtainNodeMonitorSettingArgVal(NodeStateMonitor.constants.resetModes.FAILURE_RECOVERY, device, nodeMonitoringSettings);
debug(`[onNodeStatusEvent] For device ${devicePath}, failure recovery timeout: ${timeoutDurationForFailureRecovery}`);
if (!_isNil(timeoutDurationForFailureRecovery)) {
const failureRecoveryAttemptsCount = this.getFailureRecoveryAttemptsTally(devicePath);
if (failureRecoveryAttemptsCount < NodeStateMonitor.constants.misc.MAX_RECOVERY_ATTEMPTS) {
debug(`[onNodeStatusEvent] Initiating failure recovery attempt for device ${devicePath}. Failure recovery attempts count: ${failureRecoveryAttemptsCount}`);
this.incrementFailureRecoveryAttemptTally(devicePath);
this.resetLastSeenTimerForDevice(devicePath, NodeStateMonitor.constants.resetModes.FAILURE_RECOVERY, nodeMonitoringSettings);
this.resetPingTimerForDevice(devicePath, nodeMonitoringSettings);
}
}
} break;
// TODO: Currently do not have any situations where we mark
// the node status as dead immediately on message arrival.
// case NodeStateMonitor.constants.nodeStatusTypes.DEAD:
// this.updateNodeState(devicePath, NodeStateMonitor.constants.nodeStates.DEAD);
// break;
default:
break;
}
} catch (e) {
this.adapter.k4.logging.system(`[nodeStateMonitor]/onNodeStatusEvent--Not able to update node status for nodeId: ${nodeId}. ${e.toString()}`, e instanceof Error ? e.stack.split('\n') : null);
}
}
/**
* @description Update the count of how many times a specified device
* node has progressed to the failure recovery stage during its lifetime
* @param {String} devicePath
*/
incrementFailureRecoveryAttemptTally(devicePath) {
debug(`[incrementFailureRecoveryAttemptTally] Going to tally another failure recovery attempt for device ${devicePath}`);
if (!this.failureRecoveryAttemptsByDeviceNode.has(devicePath)) {
this.failureRecoveryAttemptsByDeviceNode.set(devicePath, 0);
}
const currentNumAttempts = this.failureRecoveryAttemptsByDeviceNode.get(devicePath);
this.failureRecoveryAttemptsByDeviceNode.set(devicePath, 1 + currentNumAttempts);
}
/**
* @description Retrieve the count of how many times a specified device
* node has progressed to the failure recovery stage during its lifetime
* @param {String} devicePath
* @returns {Number}
* @throws {Error}
*/
getFailureRecoveryAttemptsTally(devicePath) {
if (!this.failureRecoveryAttemptsByDeviceNode.has(devicePath)) {
this.failureRecoveryAttemptsByDeviceNode.set(devicePath, 0);
}
return this.failureRecoveryAttemptsByDeviceNode.get(devicePath);
}
/**
* @description Resets the timer for when to send the
* next noOp ping to the device.
* @param {String} devicePath Path to the device model node
* @param {Object} [nodeMonitoringSettings]
* @throws {Error}
*/
resetPingTimerForDevice(devicePath, nodeMonitoringSettings) {
debug(`[resetPingTimerForDevice] Going to reset ping timer for Device ${devicePath}`);
let disableAutoPingsGlobally = false;
if (!_isNil(this._opts) &&
!_isNil(this._opts.disableAutoPingsGlobally)) {
disableAutoPingsGlobally = this._opts.disableAutoPingsGlobally;
}
if (disableAutoPingsGlobally === true) {
return;
}
const device = this.adapter.findBridgeDeviceByNode(devicePath);
const deviceNodeMonitoringSettings = !_isNil(nodeMonitoringSettings) ? nodeMonitoringSettings : this.extractNodeMonitoringSettings(device);
let enableAutoPing = true;
if (!_isNil(deviceNodeMonitoringSettings) &&
!_isNil(deviceNodeMonitoringSettings.enableAutoPing)) {
enableAutoPing = deviceNodeMonitoringSettings.enableAutoPing;
}
if (enableAutoPing === false) {
this.adapter.k4.logging.debug(`[NodeStateMonitor] Auto pings were disabled for ${devicePath}. Skipping ping timer reset.`);
return;
}
const pingIntervalSec = this.obtainNodeMonitorSettingArgVal('pingInterval', device, deviceNodeMonitoringSettings);
const pingAfterDead = this.obtainNodeMonitorSettingArgVal('pingAfterDead', device, deviceNodeMonitoringSettings);
if (_isNil(pingIntervalSec)) {
throw new Error(`Ping interval was not specified for device: ${devicePath}`);
}
if (this.pingTimersByDeviceNode.has(devicePath)) {
const currentTimer = this.pingTimersByDeviceNode.get(devicePath);
clearInterval(currentTimer);
debug(`[resetPingTimerForDevice] Device ${devicePath} pingTimer REMOVED: `, new Date().toISOString(), currentTimer.customId);
this.pingTimersByDeviceNode.delete(devicePath);
}
const timerCustomId = this._rng.guid({ version: 4 });
debug(`[resetPingTimerForDevice] Device ${devicePath} pingTimer CREATED: `, pingIntervalSec, new Date().toISOString(), timerCustomId);
const pingTimerObj = setInterval(() => {
debug(`[resetPingTimerForDevice] Device ${devicePath} pingTimer EXPIRED: `, new Date().toISOString(), timerCustomId);
try {
if (pingAfterDead === true || this.getNodeStateByDevicePath(devicePath) !== NodeStateMonitor.constants.nodeStates.DEAD) {
this.issuePing(devicePath);
}
} catch (e) {
this.adapter.k4.logging.system(`Failed to issue ping for device: ${devicePath}. ${e.toString()}`, e instanceof Error ? e.stack.split('\n') : null);
}
}, 1000 * pingIntervalSec);
pingTimerObj.customId = timerCustomId;
this.pingTimersByDeviceNode.set(devicePath, pingTimerObj);
}
/**
* @description Resets/refills the timer for the
* device when it has been responsive/active
* @param {String} devicePath Path to the device model node
* @param {String} [resetMode] The type of timeout to refill the timer using
* @param {Object} [nodeMonitoringSettings] The settings object for node
* monitoring timeouts, intervals, etc.
* @throws {Error}
*/
resetLastSeenTimerForDevice(devicePath, resetMode, nodeMonitoringSettings) {
debug(`[resetLastSeenTimerForDevice] Going to reset last seen timer for device ${devicePath}.`, new Date().toISOString(), this.lastSeenTimersByDeviceNode.keys());
const device = this.adapter.findBridgeDeviceByNode(devicePath);
const deviceNodeMonitoringSettings = !_isNil(nodeMonitoringSettings) ? nodeMonitoringSettings : this.extractNodeMonitoringSettings(device);
let nodeDeadTimeout = this.obtainNodeMonitorSettingArgVal(NodeStateMonitor.constants.resetModes.STANDARD_DEAD, device, deviceNodeMonitoringSettings);
debug(`[resetLastSeenTimerForDevice] Obtaining nodeDeadTimeout for device ${devicePath}. Value is: ${nodeDeadTimeout}`);
if (!_isNil(resetMode)) {
const timeoutForSpecifiedMode = this.obtainNodeMonitorSettingArgVal(resetMode, device, deviceNodeMonitoringSettings);
debug(`[resetLastSeenTimerForDevice] Obtaining timeoutForSpecifiedMode resetMode: ${resetMode} for device ${devicePath}. Value is: ${timeoutForSpecifiedMode}`);
if (!_isNil(timeoutForSpecifiedMode)) {
nodeDeadTimeout = timeoutForSpecifiedMode;
}
}
if (_isNil(nodeDeadTimeout)) {
throw new Error(`Unable to obtain node dead timeout of reset mode ${resetMode} for device: ${devicePath}`);
}
debug(`[resetLastSeenTimerForDevice] Device ${devicePath} has lastSeenTimer already?: `, this.lastSeenTimersByDeviceNode.has(devicePath));
if (this.lastSeenTimersByDeviceNode.has(devicePath)) {
const currentTimer = this.lastSeenTimersByDeviceNode.get(devicePath);
clearTimeout(currentTimer);
debug(`[resetLastSeenTimerForDevice] Device ${devicePath} lastSeenTimer REMOVED: `, new Date().toISOString(), currentTimer.customId);
this.lastSeenTimersByDeviceNode.delete(devicePath);
}
const timerCustomId = this._rng.guid({ version: 4 });
debug(`[resetLastSeenTimerForDevice] Device ${devicePath} lastSeenTimer CREATED: `, new Date().toISOString(), timerCustomId);
const timerObj = setTimeout(() => {
debug(`[resetLastSeenTimerForDevice] Device ${devicePath} lastSeenTimer EXPIRED: `, new Date().toISOString(), timerCustomId);
this.updateNodeState(devicePath, NodeStateMonitor.constants.nodeStates.DEAD);
}, 1000 * nodeDeadTimeout);
timerObj.customId = timerCustomId;
this.lastSeenTimersByDeviceNode.set(devicePath, timerObj);
debug(`[resetLastSeenTimerForDevice] Finished adding last seen timer Map entry for device ${devicePath}.`, new Date().toISOString(), this.lastSeenTimersByDeviceNode.keys());
}
/**
* @description Identifies the device model node given nodeId
* @param {Number} nodeId The target nodeId
* @returns {String} The path to the device node corresponding to the nodeId
* @throws {Error}
*/
findDevicePathById(nodeId) {
const devicePath = this.adapter.getBridgeDevicePathById(nodeId);
if (_isNil(devicePath)) {
throw new Error('Unable to find device path for this id: ' + nodeId);
}
return devicePath;
}
removeLastSeenTimerForDevice(devicePath) {
if (!this.lastSeenTimersByDeviceNode.has(devicePath)) {
return;
}
const currentTimer = this.lastSeenTimersByDeviceNode.get(devicePath);
clearTimeout(currentTimer);
this.lastSeenTimersByDeviceNode.delete(devicePath);
}
removePingTimerForDevice(devicePath) {
if (!this.pingTimersByDeviceNode.has(devicePath)) {
return;
}
const currentTimer = this.pingTimersByDeviceNode.get(devicePath);
clearInterval(currentTimer);
this.pingTimersByDeviceNode.delete(devicePath);
}
removePingTimers() {
if (!_isNil(this.pingTimersByDeviceNode)) {
this.pingTimersByDeviceNode
.forEach((v, devicePath) => this.removePingTimerForDevice(devicePath), this);
this.pingTimersByDeviceNode.clear();
}
}
removeLastSeenTimers() {
if (!_isNil(this.lastSeenTimersByDeviceNode)) {
this.lastSeenTimersByDeviceNode
.forEach((v, devicePath) => this.removeLastSeenTimerForDevice(devicePath), this);
this.lastSeenTimersByDeviceNode.clear();
}
}
close(options) {
debug('[close] Tearing down Node State Monitor: ', options);
this.closeRoutine.bind(this);
if (!_isNil(options) &&
!_isNil(options.force) &&
options.force === true) {
this._initCloseQueue.pause();
this._initCloseQueue.clear();
this._initCloseQueue.start();
}
return this._initCloseQueue.add(() => this.closeRoutine());
}
/**
* @description Tear down method
*/
closeRoutine() {
if (!_isNil(this._asyncCriticalTaskQueue)) {
this._asyncCriticalTaskQueue.pause();
this._asyncCriticalTaskQueue.clear();
this._asyncCriticalTaskQueue = null;
}
this._enableEventEmissions = false;
this.removePingTimers();
this.removeLastSeenTimers();
this.emitter.removeAllListeners();
this.removeNodeStatusAdapterEventListener();
if (!_isNil(this.mappingChangeHandler)) {
this.mappingChangeMonitor.emitter.removeListener(MappingChangeMonitor.constants.MAPPING_CHANGED_EVENT_NAME, this.mappingChangeHandler);
this.mappingChangeHandler = null;
}
if (!_isNil(this.modelChangeHandler)) {
this.modelChangeMonitor.emitter.removeListener(ModelChangeMonitor.constants.MODEL_NODES_CHANGED_EVENT_NAME, this.modelChangeHandler);
this.modelChangeHandler = null;
}
if (!_isNil(this.failureRecoveryAttemptsByDeviceNode)) {
this.failureRecoveryAttemptsByDeviceNode.clear();
}
this.mappingChangeMonitor.close();
this.modelChangeMonitor.close();
}
};
module.exports = NodeStateMonitor;