feat(core): Centralize timeouts/constants, add CLI prompt helpers, and introduce webhook/script actions with safety and SNMP refactors
This commit is contained in:
@@ -2,6 +2,9 @@ import * as snmp from 'npm:net-snmp@3.26.0';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts';
|
||||
import { UpsOidSets } from './oid-sets.ts';
|
||||
import { SNMP } from '../constants.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
import type { INupstAccessor } from '../interfaces/index.ts';
|
||||
|
||||
/**
|
||||
* Class for SNMP communication with UPS devices
|
||||
@@ -10,18 +13,18 @@ import { UpsOidSets } from './oid-sets.ts';
|
||||
export class NupstSnmp {
|
||||
// Active OID set
|
||||
private activeOIDs: IOidSet;
|
||||
// Reference to the parent Nupst instance
|
||||
private nupst: any; // Type 'any' to avoid circular dependency
|
||||
// Reference to the parent Nupst instance (uses interface to avoid circular dependency)
|
||||
private nupst: INupstAccessor | null = null;
|
||||
// Debug mode flag
|
||||
private debug: boolean = false;
|
||||
|
||||
// Default SNMP configuration
|
||||
private readonly DEFAULT_CONFIG: ISnmpConfig = {
|
||||
host: '127.0.0.1', // Default to localhost
|
||||
port: 161, // Default SNMP port
|
||||
port: SNMP.DEFAULT_PORT, // Default SNMP port
|
||||
community: 'public', // Default community string for v1/v2c
|
||||
version: 1, // SNMPv1
|
||||
timeout: 5000, // 5 seconds timeout
|
||||
timeout: SNMP.DEFAULT_TIMEOUT_MS, // 5 seconds timeout
|
||||
upsModel: 'cyberpower', // Default UPS model
|
||||
};
|
||||
|
||||
@@ -39,14 +42,14 @@ export class NupstSnmp {
|
||||
* Set reference to the main Nupst instance
|
||||
* @param nupst Reference to the main Nupst instance
|
||||
*/
|
||||
public setNupst(nupst: any): void {
|
||||
public setNupst(nupst: INupstAccessor): void {
|
||||
this.nupst = nupst;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reference to the main Nupst instance
|
||||
*/
|
||||
public getNupst(): any {
|
||||
public getNupst(): INupstAccessor | null {
|
||||
return this.nupst;
|
||||
}
|
||||
|
||||
@@ -55,7 +58,7 @@ export class NupstSnmp {
|
||||
*/
|
||||
public enableDebug(): void {
|
||||
this.debug = true;
|
||||
console.log('SNMP debug mode enabled - detailed logs will be shown');
|
||||
logger.info('SNMP debug mode enabled - detailed logs will be shown');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,7 +70,7 @@ export class NupstSnmp {
|
||||
if (config.upsModel === 'custom' && config.customOIDs) {
|
||||
this.activeOIDs = config.customOIDs;
|
||||
if (this.debug) {
|
||||
console.log('Using custom OIDs:', this.activeOIDs);
|
||||
logger.dim(`Using custom OIDs: ${JSON.stringify(this.activeOIDs)}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -77,7 +80,7 @@ export class NupstSnmp {
|
||||
this.activeOIDs = UpsOidSets.getOidSet(model);
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`Using OIDs for UPS model: ${model}`);
|
||||
logger.dim(`Using OIDs for UPS model: ${model}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,16 +98,16 @@ export class NupstSnmp {
|
||||
): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.debug) {
|
||||
console.log(
|
||||
logger.dim(
|
||||
`Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`,
|
||||
);
|
||||
console.log('Using community:', config.community);
|
||||
logger.dim(`Using community: ${config.community}`);
|
||||
}
|
||||
|
||||
// Create SNMP options based on configuration
|
||||
const options: any = {
|
||||
port: config.port,
|
||||
retries: 2, // Number of retries
|
||||
retries: SNMP.RETRIES, // Number of retries
|
||||
timeout: config.timeout,
|
||||
transport: 'udp4',
|
||||
idBitsSize: 32,
|
||||
@@ -151,7 +154,7 @@ export class NupstSnmp {
|
||||
// Fallback to noAuthNoPriv if auth details missing
|
||||
user.level = snmp.SecurityLevel.noAuthNoPriv;
|
||||
if (this.debug) {
|
||||
console.log('Warning: Missing authProtocol or authKey, falling back to noAuthNoPriv');
|
||||
logger.warn('Missing authProtocol or authKey, falling back to noAuthNoPriv');
|
||||
}
|
||||
}
|
||||
} else if (securityLevel === 'authPriv') {
|
||||
@@ -178,29 +181,23 @@ export class NupstSnmp {
|
||||
// Fallback to authNoPriv if priv details missing
|
||||
user.level = snmp.SecurityLevel.authNoPriv;
|
||||
if (this.debug) {
|
||||
console.log('Warning: Missing privProtocol or privKey, falling back to authNoPriv');
|
||||
logger.warn('Missing privProtocol or privKey, falling back to authNoPriv');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback to noAuthNoPriv if auth details missing
|
||||
user.level = snmp.SecurityLevel.noAuthNoPriv;
|
||||
if (this.debug) {
|
||||
console.log('Warning: Missing authProtocol or authKey, falling back to noAuthNoPriv');
|
||||
logger.warn('Missing authProtocol or authKey, falling back to noAuthNoPriv');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
console.log('SNMPv3 user configuration:', {
|
||||
name: user.name,
|
||||
level: Object.keys(snmp.SecurityLevel).find((key) =>
|
||||
snmp.SecurityLevel[key] === user.level
|
||||
),
|
||||
authProtocol: user.authProtocol ? 'Set' : 'Not Set',
|
||||
authKey: user.authKey ? 'Set' : 'Not Set',
|
||||
privProtocol: user.privProtocol ? 'Set' : 'Not Set',
|
||||
privKey: user.privKey ? 'Set' : 'Not Set',
|
||||
});
|
||||
const levelName = Object.keys(snmp.SecurityLevel).find((key) =>
|
||||
snmp.SecurityLevel[key] === user.level
|
||||
);
|
||||
logger.dim(`SNMPv3 user configuration: name=${user.name}, level=${levelName}, authProtocol=${user.authProtocol ? 'Set' : 'Not Set'}, privProtocol=${user.privProtocol ? 'Set' : 'Not Set'}`);
|
||||
}
|
||||
|
||||
session = snmp.createV3Session(config.host, user, options);
|
||||
@@ -219,7 +216,7 @@ export class NupstSnmp {
|
||||
|
||||
if (error) {
|
||||
if (this.debug) {
|
||||
console.error('SNMP GET error:', error);
|
||||
logger.error(`SNMP GET error: ${error}`);
|
||||
}
|
||||
reject(new Error(`SNMP GET error: ${error.message || error}`));
|
||||
return;
|
||||
@@ -227,7 +224,7 @@ export class NupstSnmp {
|
||||
|
||||
if (!varbinds || varbinds.length === 0) {
|
||||
if (this.debug) {
|
||||
console.error('No varbinds returned in response');
|
||||
logger.error('No varbinds returned in response');
|
||||
}
|
||||
reject(new Error('No varbinds returned in response'));
|
||||
return;
|
||||
@@ -240,7 +237,7 @@ export class NupstSnmp {
|
||||
varbinds[0].type === snmp.ObjectType.EndOfMibView
|
||||
) {
|
||||
if (this.debug) {
|
||||
console.error('SNMP error:', snmp.ObjectType[varbinds[0].type]);
|
||||
logger.error(`SNMP error: ${snmp.ObjectType[varbinds[0].type]}`);
|
||||
}
|
||||
reject(new Error(`SNMP error: ${snmp.ObjectType[varbinds[0].type]}`));
|
||||
return;
|
||||
@@ -262,11 +259,7 @@ export class NupstSnmp {
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
console.log('SNMP response:', {
|
||||
oid: varbinds[0].oid,
|
||||
type: varbinds[0].type,
|
||||
value: value,
|
||||
});
|
||||
logger.dim(`SNMP response: oid=${varbinds[0].oid}, type=${varbinds[0].type}, value=${value}`);
|
||||
}
|
||||
|
||||
resolve(value);
|
||||
@@ -285,30 +278,30 @@ export class NupstSnmp {
|
||||
this.setActiveOIDs(config);
|
||||
|
||||
if (this.debug) {
|
||||
console.log('---------------------------------------');
|
||||
console.log('Getting UPS status with config:');
|
||||
console.log(' Host:', config.host);
|
||||
console.log(' Port:', config.port);
|
||||
console.log(' Version:', config.version);
|
||||
console.log(' Timeout:', config.timeout, 'ms');
|
||||
console.log(' UPS Model:', config.upsModel || 'cyberpower');
|
||||
logger.dim('---------------------------------------');
|
||||
logger.dim('Getting UPS status with config:');
|
||||
logger.dim(` Host: ${config.host}`);
|
||||
logger.dim(` Port: ${config.port}`);
|
||||
logger.dim(` Version: ${config.version}`);
|
||||
logger.dim(` Timeout: ${config.timeout} ms`);
|
||||
logger.dim(` UPS Model: ${config.upsModel || 'cyberpower'}`);
|
||||
if (config.version === 1 || config.version === 2) {
|
||||
console.log(' Community:', config.community);
|
||||
logger.dim(` Community: ${config.community}`);
|
||||
} else if (config.version === 3) {
|
||||
console.log(' Security Level:', config.securityLevel);
|
||||
console.log(' Username:', config.username);
|
||||
console.log(' Auth Protocol:', config.authProtocol || 'None');
|
||||
console.log(' Privacy Protocol:', config.privProtocol || 'None');
|
||||
logger.dim(` Security Level: ${config.securityLevel}`);
|
||||
logger.dim(` Username: ${config.username}`);
|
||||
logger.dim(` Auth Protocol: ${config.authProtocol || 'None'}`);
|
||||
logger.dim(` Privacy Protocol: ${config.privProtocol || 'None'}`);
|
||||
}
|
||||
console.log('Using OIDs:');
|
||||
console.log(' Power Status:', this.activeOIDs.POWER_STATUS);
|
||||
console.log(' Battery Capacity:', this.activeOIDs.BATTERY_CAPACITY);
|
||||
console.log(' Battery Runtime:', this.activeOIDs.BATTERY_RUNTIME);
|
||||
console.log(' Output Load:', this.activeOIDs.OUTPUT_LOAD);
|
||||
console.log(' Output Power:', this.activeOIDs.OUTPUT_POWER);
|
||||
console.log(' Output Voltage:', this.activeOIDs.OUTPUT_VOLTAGE);
|
||||
console.log(' Output Current:', this.activeOIDs.OUTPUT_CURRENT);
|
||||
console.log('---------------------------------------');
|
||||
logger.dim('Using OIDs:');
|
||||
logger.dim(` Power Status: ${this.activeOIDs.POWER_STATUS}`);
|
||||
logger.dim(` Battery Capacity: ${this.activeOIDs.BATTERY_CAPACITY}`);
|
||||
logger.dim(` Battery Runtime: ${this.activeOIDs.BATTERY_RUNTIME}`);
|
||||
logger.dim(` Output Load: ${this.activeOIDs.OUTPUT_LOAD}`);
|
||||
logger.dim(` Output Power: ${this.activeOIDs.OUTPUT_POWER}`);
|
||||
logger.dim(` Output Voltage: ${this.activeOIDs.OUTPUT_VOLTAGE}`);
|
||||
logger.dim(` Output Current: ${this.activeOIDs.OUTPUT_CURRENT}`);
|
||||
logger.dim('---------------------------------------');
|
||||
}
|
||||
|
||||
// Get all values with independent retry logic
|
||||
@@ -365,7 +358,7 @@ export class NupstSnmp {
|
||||
if (outputPower === 0 && processedVoltage > 0 && processedCurrent > 0) {
|
||||
processedPower = Math.round(processedVoltage * processedCurrent);
|
||||
if (this.debug) {
|
||||
console.log(
|
||||
logger.dim(
|
||||
`Calculated power from V×I: ${processedVoltage}V × ${processedCurrent}A = ${processedPower}W`,
|
||||
);
|
||||
}
|
||||
@@ -391,27 +384,26 @@ export class NupstSnmp {
|
||||
};
|
||||
|
||||
if (this.debug) {
|
||||
console.log('---------------------------------------');
|
||||
console.log('UPS status result:');
|
||||
console.log(' Power Status:', result.powerStatus);
|
||||
console.log(' Battery Capacity:', result.batteryCapacity + '%');
|
||||
console.log(' Battery Runtime:', result.batteryRuntime, 'minutes');
|
||||
console.log(' Output Load:', result.outputLoad + '%');
|
||||
console.log(' Output Power:', result.outputPower, 'watts');
|
||||
console.log(' Output Voltage:', result.outputVoltage, 'volts');
|
||||
console.log(' Output Current:', result.outputCurrent, 'amps');
|
||||
console.log('---------------------------------------');
|
||||
logger.dim('---------------------------------------');
|
||||
logger.dim('UPS status result:');
|
||||
logger.dim(` Power Status: ${result.powerStatus}`);
|
||||
logger.dim(` Battery Capacity: ${result.batteryCapacity}%`);
|
||||
logger.dim(` Battery Runtime: ${result.batteryRuntime} minutes`);
|
||||
logger.dim(` Output Load: ${result.outputLoad}%`);
|
||||
logger.dim(` Output Power: ${result.outputPower} watts`);
|
||||
logger.dim(` Output Voltage: ${result.outputVoltage} volts`);
|
||||
logger.dim(` Output Current: ${result.outputCurrent} amps`);
|
||||
logger.dim('---------------------------------------');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (this.debug) {
|
||||
console.error('---------------------------------------');
|
||||
console.error(
|
||||
'Error getting UPS status:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
logger.error('---------------------------------------');
|
||||
logger.error(
|
||||
`Error getting UPS status: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
console.error('---------------------------------------');
|
||||
logger.error('---------------------------------------');
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to get UPS status: ${error instanceof Error ? error.message : String(error)}`,
|
||||
@@ -433,26 +425,25 @@ export class NupstSnmp {
|
||||
): Promise<any> {
|
||||
if (oid === '') {
|
||||
if (this.debug) {
|
||||
console.log(`No OID provided for ${description}, skipping`);
|
||||
logger.dim(`No OID provided for ${description}, skipping`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`Getting ${description} OID: ${oid}`);
|
||||
logger.dim(`Getting ${description} OID: ${oid}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const value = await this.snmpGet(oid, config);
|
||||
if (this.debug) {
|
||||
console.log(`${description} value:`, value);
|
||||
logger.dim(`${description} value: ${value}`);
|
||||
}
|
||||
return value;
|
||||
} catch (error) {
|
||||
if (this.debug) {
|
||||
console.error(
|
||||
`Error getting ${description}:`,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
logger.error(
|
||||
`Error getting ${description}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -468,7 +459,7 @@ export class NupstSnmp {
|
||||
|
||||
// Return a default value if all attempts fail
|
||||
if (this.debug) {
|
||||
console.log(`Using default value 0 for ${description}`);
|
||||
logger.dim(`Using default value 0 for ${description}`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -487,7 +478,7 @@ export class NupstSnmp {
|
||||
config: ISnmpConfig,
|
||||
): Promise<any> {
|
||||
if (this.debug) {
|
||||
console.log(`Retrying ${description} with fallback security level...`);
|
||||
logger.dim(`Retrying ${description} with fallback security level...`);
|
||||
}
|
||||
|
||||
// Try with authNoPriv if current level is authPriv
|
||||
@@ -495,18 +486,17 @@ export class NupstSnmp {
|
||||
const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' };
|
||||
try {
|
||||
if (this.debug) {
|
||||
console.log(`Retrying with authNoPriv security level`);
|
||||
logger.dim(`Retrying with authNoPriv security level`);
|
||||
}
|
||||
const value = await this.snmpGet(oid, retryConfig);
|
||||
if (this.debug) {
|
||||
console.log(`${description} retry value:`, value);
|
||||
logger.dim(`${description} retry value: ${value}`);
|
||||
}
|
||||
return value;
|
||||
} catch (retryError) {
|
||||
if (this.debug) {
|
||||
console.error(
|
||||
`Retry failed for ${description}:`,
|
||||
retryError instanceof Error ? retryError.message : String(retryError),
|
||||
logger.error(
|
||||
`Retry failed for ${description}: ${retryError instanceof Error ? retryError.message : String(retryError)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -517,18 +507,17 @@ export class NupstSnmp {
|
||||
const retryConfig = { ...config, securityLevel: 'noAuthNoPriv' as 'noAuthNoPriv' };
|
||||
try {
|
||||
if (this.debug) {
|
||||
console.log(`Retrying with noAuthNoPriv security level`);
|
||||
logger.dim(`Retrying with noAuthNoPriv security level`);
|
||||
}
|
||||
const value = await this.snmpGet(oid, retryConfig);
|
||||
if (this.debug) {
|
||||
console.log(`${description} retry value:`, value);
|
||||
logger.dim(`${description} retry value: ${value}`);
|
||||
}
|
||||
return value;
|
||||
} catch (retryError) {
|
||||
if (this.debug) {
|
||||
console.error(
|
||||
`Retry failed for ${description}:`,
|
||||
retryError instanceof Error ? retryError.message : String(retryError),
|
||||
logger.error(
|
||||
`Retry failed for ${description}: ${retryError instanceof Error ? retryError.message : String(retryError)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -554,21 +543,20 @@ export class NupstSnmp {
|
||||
const standardOIDs = UpsOidSets.getStandardOids();
|
||||
|
||||
if (this.debug) {
|
||||
console.log(
|
||||
logger.dim(
|
||||
`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`,
|
||||
);
|
||||
}
|
||||
|
||||
const standardValue = await this.snmpGet(standardOIDs[description], config);
|
||||
if (this.debug) {
|
||||
console.log(`${description} standard OID value:`, standardValue);
|
||||
logger.dim(`${description} standard OID value: ${standardValue}`);
|
||||
}
|
||||
return standardValue;
|
||||
} catch (stdError) {
|
||||
if (this.debug) {
|
||||
console.error(
|
||||
`Standard OID retry failed for ${description}:`,
|
||||
stdError instanceof Error ? stdError.message : String(stdError),
|
||||
logger.error(
|
||||
`Standard OID retry failed for ${description}: ${stdError instanceof Error ? stdError.message : String(stdError)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -623,14 +611,14 @@ export class NupstSnmp {
|
||||
batteryRuntime: number,
|
||||
): number {
|
||||
if (this.debug) {
|
||||
console.log('Raw runtime value:', batteryRuntime);
|
||||
logger.dim(`Raw runtime value: ${batteryRuntime}`);
|
||||
}
|
||||
|
||||
if (upsModel === 'cyberpower' && batteryRuntime > 0) {
|
||||
// CyberPower: TimeTicks is in 1/100 seconds, convert to minutes
|
||||
const minutes = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute
|
||||
if (this.debug) {
|
||||
console.log(
|
||||
logger.dim(
|
||||
`Converting CyberPower runtime from ${batteryRuntime} ticks to ${minutes} minutes`,
|
||||
);
|
||||
}
|
||||
@@ -639,7 +627,7 @@ export class NupstSnmp {
|
||||
// Eaton: Runtime is in seconds, convert to minutes
|
||||
const minutes = Math.floor(batteryRuntime / 60);
|
||||
if (this.debug) {
|
||||
console.log(
|
||||
logger.dim(
|
||||
`Converting Eaton runtime from ${batteryRuntime} seconds to ${minutes} minutes`,
|
||||
);
|
||||
}
|
||||
@@ -648,7 +636,7 @@ export class NupstSnmp {
|
||||
// Generic conversion for large tick values (likely TimeTicks)
|
||||
const minutes = Math.floor(batteryRuntime / 6000);
|
||||
if (this.debug) {
|
||||
console.log(`Converting ${batteryRuntime} ticks to ${minutes} minutes`);
|
||||
logger.dim(`Converting ${batteryRuntime} ticks to ${minutes} minutes`);
|
||||
}
|
||||
return minutes;
|
||||
}
|
||||
@@ -667,14 +655,14 @@ export class NupstSnmp {
|
||||
outputVoltage: number,
|
||||
): number {
|
||||
if (this.debug) {
|
||||
console.log('Raw voltage value:', outputVoltage);
|
||||
logger.dim(`Raw voltage value: ${outputVoltage}`);
|
||||
}
|
||||
|
||||
if (upsModel === 'cyberpower' && outputVoltage > 0) {
|
||||
// CyberPower: Voltage is in 0.1V, convert to volts
|
||||
const volts = outputVoltage / 10;
|
||||
if (this.debug) {
|
||||
console.log(
|
||||
logger.dim(
|
||||
`Converting CyberPower voltage from ${outputVoltage} (0.1V) to ${volts} volts`,
|
||||
);
|
||||
}
|
||||
@@ -695,14 +683,14 @@ export class NupstSnmp {
|
||||
outputCurrent: number,
|
||||
): number {
|
||||
if (this.debug) {
|
||||
console.log('Raw current value:', outputCurrent);
|
||||
logger.dim(`Raw current value: ${outputCurrent}`);
|
||||
}
|
||||
|
||||
if (upsModel === 'cyberpower' && outputCurrent > 0) {
|
||||
// CyberPower: Current is in 0.1A, convert to amps
|
||||
const amps = outputCurrent / 10;
|
||||
if (this.debug) {
|
||||
console.log(
|
||||
logger.dim(
|
||||
`Converting CyberPower current from ${outputCurrent} (0.1A) to ${amps} amps`,
|
||||
);
|
||||
}
|
||||
@@ -711,7 +699,7 @@ export class NupstSnmp {
|
||||
// RFC 1628 standard: Current is in 0.1A, convert to amps
|
||||
const amps = outputCurrent / 10;
|
||||
if (this.debug) {
|
||||
console.log(
|
||||
logger.dim(
|
||||
`Converting RFC 1628 current from ${outputCurrent} (0.1A) to ${amps} amps`,
|
||||
);
|
||||
}
|
||||
@@ -720,7 +708,7 @@ export class NupstSnmp {
|
||||
|
||||
// Eaton XUPS-MIB and APC PowerNet report current directly in RMS Amps (no scaling needed)
|
||||
if ((upsModel === 'eaton' || upsModel === 'apc') && this.debug && outputCurrent > 0) {
|
||||
console.log(`${upsModel.toUpperCase()} current already in RMS Amps: ${outputCurrent}A`);
|
||||
logger.dim(`${upsModel.toUpperCase()} current already in RMS Amps: ${outputCurrent}A`);
|
||||
}
|
||||
|
||||
return outputCurrent;
|
||||
|
||||
Reference in New Issue
Block a user