Files
nupst/ts/snmp/manager.ts

729 lines
24 KiB
TypeScript
Raw Normal View History

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';
2025-03-25 09:06:23 +00:00
/**
* Class for SNMP communication with UPS devices
* Main entry point for SNMP functionality
*/
export class NupstSnmp {
// Active OID set
private activeOIDs: IOidSet;
// Reference to the parent Nupst instance (uses interface to avoid circular dependency)
private nupst: INupstAccessor | null = null;
2025-03-26 13:13:01 +00:00
// Debug mode flag
private debug: boolean = false;
2025-03-25 09:06:23 +00:00
// Default SNMP configuration
private readonly DEFAULT_CONFIG: ISnmpConfig = {
2025-03-25 09:06:23 +00:00
host: '127.0.0.1', // Default to localhost
port: SNMP.DEFAULT_PORT, // Default SNMP port
2025-03-25 09:06:23 +00:00
community: 'public', // Default community string for v1/v2c
version: 1, // SNMPv1
timeout: SNMP.DEFAULT_TIMEOUT_MS, // 5 seconds timeout
2025-03-25 09:06:23 +00:00
upsModel: 'cyberpower', // Default UPS model
};
/**
* Create a new SNMP manager
* @param debug Whether to enable debug mode
*/
constructor(debug = false) {
this.debug = debug;
// Set default OID set
this.activeOIDs = UpsOidSets.getOidSet('cyberpower');
}
/**
* Set reference to the main Nupst instance
* @param nupst Reference to the main Nupst instance
*/
public setNupst(nupst: INupstAccessor): void {
this.nupst = nupst;
}
/**
* Get reference to the main Nupst instance
*/
public getNupst(): INupstAccessor | null {
return this.nupst;
}
2025-03-26 13:13:01 +00:00
/**
* Enable debug mode
*/
public enableDebug(): void {
this.debug = true;
logger.info('SNMP debug mode enabled - detailed logs will be shown');
2025-03-26 13:13:01 +00:00
}
2025-03-25 09:06:23 +00:00
/**
* Set active OID set based on UPS model
* @param config SNMP configuration
*/
private setActiveOIDs(config: ISnmpConfig): void {
2025-03-25 09:06:23 +00:00
// If custom OIDs are provided, use them
if (config.upsModel === 'custom' && config.customOIDs) {
this.activeOIDs = config.customOIDs;
if (this.debug) {
logger.dim(`Using custom OIDs: ${JSON.stringify(this.activeOIDs)}`);
2025-03-25 09:06:23 +00:00
}
return;
}
2025-03-25 09:06:23 +00:00
// Use OIDs for the specified UPS model or default to Cyberpower
const model = config.upsModel || 'cyberpower';
this.activeOIDs = UpsOidSets.getOidSet(model);
2025-03-25 09:06:23 +00:00
if (this.debug) {
logger.dim(`Using OIDs for UPS model: ${model}`);
2025-03-25 09:06:23 +00:00
}
}
/**
2025-03-26 13:13:01 +00:00
* Send an SNMP GET request using the net-snmp package
2025-03-25 09:06:23 +00:00
* @param oid OID to query
* @param config SNMP configuration
2025-03-26 13:13:01 +00:00
* @param retryCount Current retry count (unused in this implementation)
2025-03-25 09:06:23 +00:00
* @returns Promise resolving to the SNMP response value
*/
public snmpGet(
oid: string,
config = this.DEFAULT_CONFIG,
retryCount = 0,
2025-03-26 13:13:01 +00:00
): Promise<any> {
2025-03-25 09:06:23 +00:00
return new Promise((resolve, reject) => {
2025-03-26 13:13:01 +00:00
if (this.debug) {
logger.dim(
`Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`,
);
logger.dim(`Using community: ${config.community}`);
2025-03-26 13:13:01 +00:00
}
// Create SNMP options based on configuration
const options: any = {
port: config.port,
retries: SNMP.RETRIES, // Number of retries
2025-03-26 13:13:01 +00:00
timeout: config.timeout,
transport: 'udp4',
idBitsSize: 32,
context: config.context || '',
2025-03-26 13:13:01 +00:00
};
// Set version based on config
if (config.version === 1) {
options.version = snmp.Version1;
} else if (config.version === 2) {
2025-03-26 13:13:01 +00:00
options.version = snmp.Version2c;
2025-03-25 09:06:23 +00:00
} else {
2025-03-26 13:13:01 +00:00
options.version = snmp.Version3;
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
// Create appropriate session based on SNMP version
let session;
2025-03-26 13:13:01 +00:00
if (config.version === 3) {
// For SNMPv3, we need to set up authentication and privacy
// For SNMPv3, we need a valid security level
const securityLevel = config.securityLevel || 'noAuthNoPriv';
// Create the user object with required structure for net-snmp
const user: any = {
name: config.username || '',
2025-03-26 13:13:01 +00:00
};
// Set security level
if (securityLevel === 'noAuthNoPriv') {
user.level = snmp.SecurityLevel.noAuthNoPriv;
} else if (securityLevel === 'authNoPriv') {
user.level = snmp.SecurityLevel.authNoPriv;
// Set auth protocol - must provide both protocol and key
if (config.authProtocol && config.authKey) {
if (config.authProtocol === 'MD5') {
user.authProtocol = snmp.AuthProtocols.md5;
} else if (config.authProtocol === 'SHA') {
user.authProtocol = snmp.AuthProtocols.sha;
}
user.authKey = config.authKey;
} else {
// Fallback to noAuthNoPriv if auth details missing
user.level = snmp.SecurityLevel.noAuthNoPriv;
if (this.debug) {
logger.warn('Missing authProtocol or authKey, falling back to noAuthNoPriv');
}
}
} else if (securityLevel === 'authPriv') {
user.level = snmp.SecurityLevel.authPriv;
// Set auth protocol - must provide both protocol and key
if (config.authProtocol && config.authKey) {
if (config.authProtocol === 'MD5') {
user.authProtocol = snmp.AuthProtocols.md5;
} else if (config.authProtocol === 'SHA') {
user.authProtocol = snmp.AuthProtocols.sha;
}
user.authKey = config.authKey;
// Set privacy protocol - must provide both protocol and key
if (config.privProtocol && config.privKey) {
if (config.privProtocol === 'DES') {
user.privProtocol = snmp.PrivProtocols.des;
} else if (config.privProtocol === 'AES') {
user.privProtocol = snmp.PrivProtocols.aes;
}
user.privKey = config.privKey;
} else {
// Fallback to authNoPriv if priv details missing
user.level = snmp.SecurityLevel.authNoPriv;
if (this.debug) {
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) {
logger.warn('Missing authProtocol or authKey, falling back to noAuthNoPriv');
}
}
}
if (this.debug) {
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'}`,
);
}
2025-03-26 13:13:01 +00:00
session = snmp.createV3Session(config.host, user, options);
} else {
// For SNMPv1/v2c, we use the community string
session = snmp.createSession(config.host, config.community || 'public', options);
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
// Convert the OID string to an array of OIDs if multiple OIDs are needed
const oids = [oid];
// Send the GET request
session.get(oids, (error: any, varbinds: any[]) => {
// Close the session to release resources
session.close();
if (error) {
if (this.debug) {
logger.error(`SNMP GET error: ${error}`);
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
reject(new Error(`SNMP GET error: ${error.message || error}`));
return;
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
if (!varbinds || varbinds.length === 0) {
2025-03-25 09:06:23 +00:00
if (this.debug) {
logger.error('No varbinds returned in response');
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
reject(new Error('No varbinds returned in response'));
return;
}
// Check for SNMP errors in the response
if (
varbinds[0].type === snmp.ObjectType.NoSuchObject ||
varbinds[0].type === snmp.ObjectType.NoSuchInstance ||
varbinds[0].type === snmp.ObjectType.EndOfMibView
) {
2025-03-25 09:06:23 +00:00
if (this.debug) {
logger.error(`SNMP error: ${snmp.ObjectType[varbinds[0].type]}`);
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
reject(new Error(`SNMP error: ${snmp.ObjectType[varbinds[0].type]}`));
return;
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
// Process the response value based on its type
let value = varbinds[0].value;
// Handle specific types that might need conversion
if (Buffer.isBuffer(value)) {
// If value is a Buffer, try to convert it to a string if it's printable ASCII
const isPrintableAscii = value.every((byte: number) => byte >= 32 && byte <= 126);
2025-03-26 13:13:01 +00:00
if (isPrintableAscii) {
value = value.toString();
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
} else if (typeof value === 'bigint') {
// Convert BigInt to a normal number or string if needed
value = Number(value);
}
if (this.debug) {
logger.dim(
`SNMP response: oid=${varbinds[0].oid}, type=${varbinds[0].type}, value=${value}`,
);
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
resolve(value);
2025-03-25 09:06:23 +00:00
});
});
}
/**
* Get the current status of the UPS
* @param config SNMP configuration
* @returns Promise resolving to the UPS status
*/
public async getUpsStatus(config = this.DEFAULT_CONFIG): Promise<IUpsStatus> {
2025-03-25 09:06:23 +00:00
try {
// Set active OID set based on UPS model in config
this.setActiveOIDs(config);
2025-03-25 09:06:23 +00:00
if (this.debug) {
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'}`);
2025-03-25 09:06:23 +00:00
if (config.version === 1 || config.version === 2) {
logger.dim(` Community: ${config.community}`);
2025-03-25 09:06:23 +00:00
} else if (config.version === 3) {
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'}`);
2025-03-25 09:06:23 +00:00
}
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('---------------------------------------');
2025-03-25 09:06:23 +00:00
}
2025-03-25 09:06:23 +00:00
// Get all values with independent retry logic
const powerStatusValue = await this.getSNMPValueWithRetry(
this.activeOIDs.POWER_STATUS,
'power status',
config,
);
const batteryCapacity = await this.getSNMPValueWithRetry(
this.activeOIDs.BATTERY_CAPACITY,
'battery capacity',
config,
) || 0;
const batteryRuntime = await this.getSNMPValueWithRetry(
this.activeOIDs.BATTERY_RUNTIME,
'battery runtime',
config,
) || 0;
// Get power draw metrics
const outputLoad = await this.getSNMPValueWithRetry(
this.activeOIDs.OUTPUT_LOAD,
'output load',
config,
) || 0;
const outputPower = await this.getSNMPValueWithRetry(
this.activeOIDs.OUTPUT_POWER,
'output power',
config,
) || 0;
const outputVoltage = await this.getSNMPValueWithRetry(
this.activeOIDs.OUTPUT_VOLTAGE,
'output voltage',
config,
) || 0;
const outputCurrent = await this.getSNMPValueWithRetry(
this.activeOIDs.OUTPUT_CURRENT,
'output current',
config,
) || 0;
2025-03-25 09:06:23 +00:00
// Determine power status - handle different values for different UPS models
2025-03-26 13:13:01 +00:00
const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue);
// Convert to minutes for UPS models with different time units
2025-03-26 13:13:01 +00:00
const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime);
// Process power metrics with vendor-specific scaling
const processedVoltage = this.processVoltageValue(config.upsModel, outputVoltage);
const processedCurrent = this.processCurrentValue(config.upsModel, outputCurrent);
// Calculate power from voltage × current if not provided by UPS
let processedPower = outputPower;
if (outputPower === 0 && processedVoltage > 0 && processedCurrent > 0) {
processedPower = Math.round(processedVoltage * processedCurrent);
if (this.debug) {
logger.dim(
`Calculated power from V×I: ${processedVoltage}V × ${processedCurrent}A = ${processedPower}W`,
);
}
}
2025-03-25 09:06:23 +00:00
const result = {
powerStatus,
batteryCapacity,
batteryRuntime: processedRuntime,
outputLoad,
outputPower: processedPower,
outputVoltage: processedVoltage,
outputCurrent: processedCurrent,
2025-03-25 09:06:23 +00:00
raw: {
powerStatus: powerStatusValue,
batteryCapacity,
batteryRuntime,
outputLoad,
outputPower,
outputVoltage,
outputCurrent,
2025-03-25 09:06:23 +00:00
},
};
2025-03-25 09:06:23 +00:00
if (this.debug) {
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('---------------------------------------');
2025-03-25 09:06:23 +00:00
}
2025-03-25 09:06:23 +00:00
return result;
} catch (error) {
if (this.debug) {
logger.error('---------------------------------------');
logger.error(
`Error getting UPS status: ${error instanceof Error ? error.message : String(error)}`,
);
logger.error('---------------------------------------');
2025-03-25 09:06:23 +00:00
}
throw new Error(
`Failed to get UPS status: ${error instanceof Error ? error.message : String(error)}`,
);
2025-03-25 09:06:23 +00:00
}
}
/**
2025-03-26 13:13:01 +00:00
* Helper method to get SNMP value with retry and fallback logic
* @param oid OID to query
* @param description Description of the value for logging
2025-03-25 09:06:23 +00:00
* @param config SNMP configuration
2025-03-26 13:13:01 +00:00
* @returns Promise resolving to the SNMP value
2025-03-25 09:06:23 +00:00
*/
2025-03-26 13:13:01 +00:00
private async getSNMPValueWithRetry(
oid: string,
description: string,
config: ISnmpConfig,
2025-03-26 13:13:01 +00:00
): Promise<any> {
if (oid === '') {
if (this.debug) {
logger.dim(`No OID provided for ${description}, skipping`);
2025-03-26 13:13:01 +00:00
}
return 0;
}
2025-03-26 13:13:01 +00:00
if (this.debug) {
logger.dim(`Getting ${description} OID: ${oid}`);
2025-03-26 13:13:01 +00:00
}
2025-03-26 13:13:01 +00:00
try {
const value = await this.snmpGet(oid, config);
if (this.debug) {
logger.dim(`${description} value: ${value}`);
2025-03-26 13:13:01 +00:00
}
return value;
} catch (error) {
if (this.debug) {
logger.error(
`Error getting ${description}: ${error instanceof Error ? error.message : String(error)}`,
);
2025-03-26 13:13:01 +00:00
}
2025-03-26 13:13:01 +00:00
// If we're using SNMPv3, try with different security levels
if (config.version === 3) {
return await this.tryFallbackSecurityLevels(oid, description, config);
}
2025-03-26 13:13:01 +00:00
// Try with standard OIDs as fallback
if (config.upsModel !== 'custom') {
return await this.tryStandardOids(oid, description, config);
}
2025-03-26 13:13:01 +00:00
// Return a default value if all attempts fail
2025-03-25 09:06:23 +00:00
if (this.debug) {
logger.dim(`Using default value 0 for ${description}`);
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
return 0;
}
}
/**
* Try fallback security levels for SNMPv3
* @param oid OID to query
* @param description Description of the value for logging
* @param config SNMP configuration
* @returns Promise resolving to the SNMP value
*/
private async tryFallbackSecurityLevels(
oid: string,
description: string,
config: ISnmpConfig,
2025-03-26 13:13:01 +00:00
): Promise<any> {
if (this.debug) {
logger.dim(`Retrying ${description} with fallback security level...`);
2025-03-26 13:13:01 +00:00
}
2025-03-26 13:13:01 +00:00
// Try with authNoPriv if current level is authPriv
if (config.securityLevel === 'authPriv') {
const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' };
try {
2025-03-25 09:06:23 +00:00
if (this.debug) {
logger.dim(`Retrying with authNoPriv security level`);
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
const value = await this.snmpGet(oid, retryConfig);
2025-03-25 09:06:23 +00:00
if (this.debug) {
logger.dim(`${description} retry value: ${value}`);
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
return value;
} catch (retryError) {
if (this.debug) {
logger.error(
`Retry failed for ${description}: ${
retryError instanceof Error ? retryError.message : String(retryError)
}`,
);
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
}
}
2025-03-26 13:13:01 +00:00
// Try with noAuthNoPriv as a last resort
if (config.securityLevel === 'authPriv' || config.securityLevel === 'authNoPriv') {
const retryConfig = { ...config, securityLevel: 'noAuthNoPriv' as 'noAuthNoPriv' };
try {
2025-03-25 09:06:23 +00:00
if (this.debug) {
logger.dim(`Retrying with noAuthNoPriv security level`);
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
const value = await this.snmpGet(oid, retryConfig);
if (this.debug) {
logger.dim(`${description} retry value: ${value}`);
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
return value;
} catch (retryError) {
if (this.debug) {
logger.error(
`Retry failed for ${description}: ${
retryError instanceof Error ? retryError.message : String(retryError)
}`,
);
2025-03-26 13:13:01 +00:00
}
}
}
2025-03-26 13:13:01 +00:00
return 0;
}
/**
* Try standard OIDs as fallback
* @param oid OID to query
* @param description Description of the value for logging
* @param config SNMP configuration
* @returns Promise resolving to the SNMP value
*/
private async tryStandardOids(
oid: string,
description: string,
config: ISnmpConfig,
2025-03-26 13:13:01 +00:00
): Promise<any> {
try {
// Try RFC 1628 standard UPS MIB OIDs
const standardOIDs = UpsOidSets.getStandardOids();
2025-03-26 13:13:01 +00:00
if (this.debug) {
logger.dim(
`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`,
);
2025-03-26 13:13:01 +00:00
}
2025-03-26 13:13:01 +00:00
const standardValue = await this.snmpGet(standardOIDs[description], config);
if (this.debug) {
logger.dim(`${description} standard OID value: ${standardValue}`);
2025-03-26 13:13:01 +00:00
}
return standardValue;
} catch (stdError) {
if (this.debug) {
logger.error(
`Standard OID retry failed for ${description}: ${
stdError instanceof Error ? stdError.message : String(stdError)
}`,
);
2025-03-26 13:13:01 +00:00
}
}
2025-03-26 13:13:01 +00:00
return 0;
2025-03-25 09:06:23 +00:00
}
2025-03-26 13:13:01 +00:00
/**
* Determine power status based on UPS model and raw value
* Uses the value mappings defined in the OID sets
2025-03-26 13:13:01 +00:00
* @param upsModel UPS model
* @param powerStatusValue Raw power status value
* @returns Standardized power status
*/
private determinePowerStatus(
upsModel: TUpsModel | undefined,
powerStatusValue: number,
2025-03-26 13:13:01 +00:00
): 'online' | 'onBattery' | 'unknown' {
// Get the OID set for this UPS model
if (upsModel && upsModel !== 'custom') {
const oidSet = UpsOidSets.getOidSet(upsModel);
// Use the value mappings if available
if (oidSet.POWER_STATUS_VALUES) {
if (powerStatusValue === oidSet.POWER_STATUS_VALUES.online) {
return 'online';
} else if (powerStatusValue === oidSet.POWER_STATUS_VALUES.onBattery) {
return 'onBattery';
}
2025-03-26 13:13:01 +00:00
}
}
// Fallback for custom or undefined models (RFC 1628 standard)
// upsOutputSource: 3=normal (mains), 5=battery
if (powerStatusValue === 3) {
return 'online';
} else if (powerStatusValue === 5) {
return 'onBattery';
}
2025-03-26 13:13:01 +00:00
return 'unknown';
}
/**
* Process runtime value based on UPS model
* @param upsModel UPS model
* @param batteryRuntime Raw battery runtime value
* @returns Processed runtime in minutes
*/
private processRuntimeValue(
upsModel: TUpsModel | undefined,
batteryRuntime: number,
2025-03-26 13:13:01 +00:00
): number {
if (this.debug) {
logger.dim(`Raw runtime value: ${batteryRuntime}`);
2025-03-26 13:13:01 +00:00
}
2025-03-26 13:13:01 +00:00
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) {
logger.dim(
`Converting CyberPower runtime from ${batteryRuntime} ticks to ${minutes} minutes`,
);
2025-03-26 13:13:01 +00:00
}
return minutes;
} else if (upsModel === 'eaton' && batteryRuntime > 0) {
// Eaton: Runtime is in seconds, convert to minutes
const minutes = Math.floor(batteryRuntime / 60);
if (this.debug) {
logger.dim(
`Converting Eaton runtime from ${batteryRuntime} seconds to ${minutes} minutes`,
);
2025-03-26 13:13:01 +00:00
}
return minutes;
} else if (batteryRuntime > 10000) {
// Generic conversion for large tick values (likely TimeTicks)
const minutes = Math.floor(batteryRuntime / 6000);
if (this.debug) {
logger.dim(`Converting ${batteryRuntime} ticks to ${minutes} minutes`);
2025-03-26 13:13:01 +00:00
}
return minutes;
}
2025-03-26 13:13:01 +00:00
return batteryRuntime;
}
/**
* Process voltage value based on UPS model
* @param upsModel UPS model
* @param outputVoltage Raw output voltage value
* @returns Processed voltage in volts
*/
private processVoltageValue(
upsModel: TUpsModel | undefined,
outputVoltage: number,
): number {
if (this.debug) {
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) {
logger.dim(
`Converting CyberPower voltage from ${outputVoltage} (0.1V) to ${volts} volts`,
);
}
return volts;
}
return outputVoltage;
}
/**
* Process current value based on UPS model
* @param upsModel UPS model
* @param outputCurrent Raw output current value
* @returns Processed current in amps
*/
private processCurrentValue(
upsModel: TUpsModel | undefined,
outputCurrent: number,
): number {
if (this.debug) {
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) {
logger.dim(
`Converting CyberPower current from ${outputCurrent} (0.1A) to ${amps} amps`,
);
}
return amps;
} else if ((upsModel === 'tripplite' || upsModel === 'liebert') && outputCurrent > 0) {
// RFC 1628 standard: Current is in 0.1A, convert to amps
const amps = outputCurrent / 10;
if (this.debug) {
logger.dim(
`Converting RFC 1628 current from ${outputCurrent} (0.1A) to ${amps} amps`,
);
}
return amps;
}
// Eaton XUPS-MIB and APC PowerNet report current directly in RMS Amps (no scaling needed)
if ((upsModel === 'eaton' || upsModel === 'apc') && this.debug && outputCurrent > 0) {
logger.dim(`${upsModel.toUpperCase()} current already in RMS Amps: ${outputCurrent}A`);
}
return outputCurrent;
}
}