feat(core): Centralize timeouts/constants, add CLI prompt helpers, and introduce webhook/script actions with safety and SNMP refactors
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/nupst',
|
||||
version: '5.1.11',
|
||||
version: '5.2.0',
|
||||
description: 'Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies'
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ScriptAction } from './script-action.ts';
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { IActionConfig, IActionContext, TPowerStatus } from './base-action.ts';
|
||||
export type { IWebhookPayload } from './webhook-action.ts';
|
||||
export { Action } from './base-action.ts';
|
||||
export { ShutdownAction } from './shutdown-action.ts';
|
||||
export { WebhookAction } from './webhook-action.ts';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { Action, type IActionConfig, type IActionContext } from './base-action.ts';
|
||||
import { Action, type IActionContext } from './base-action.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
import { SHUTDOWN, UI } from '../constants.ts';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -15,6 +16,81 @@ const execFileAsync = promisify(execFile);
|
||||
export class ShutdownAction extends Action {
|
||||
readonly type = 'shutdown';
|
||||
|
||||
/**
|
||||
* Override shouldExecute to add shutdown-specific safety checks
|
||||
*
|
||||
* Key safety rules:
|
||||
* 1. Shutdown should NEVER trigger unless UPS is actually on battery
|
||||
* (low battery while on grid power is not an emergency - it's charging)
|
||||
* 2. For power status changes, only trigger on transitions TO onBattery from online
|
||||
* (ignore unknown → online at startup, and power restoration events)
|
||||
* 3. For threshold violations, verify UPS is on battery before acting
|
||||
*
|
||||
* @param context Action context with UPS state
|
||||
* @returns True if shutdown should execute
|
||||
*/
|
||||
protected override shouldExecute(context: IActionContext): boolean {
|
||||
const mode = this.config.triggerMode || 'powerChangesAndThresholds';
|
||||
|
||||
// CRITICAL SAFETY CHECK: Shutdown should NEVER trigger unless UPS is on battery
|
||||
// A low battery while on grid power is not an emergency (the battery is charging)
|
||||
if (context.powerStatus !== 'onBattery') {
|
||||
logger.info(`Shutdown action skipped: UPS is not on battery (status: ${context.powerStatus})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle threshold violations (UPS is confirmed on battery at this point)
|
||||
if (context.triggerReason === 'thresholdViolation') {
|
||||
// 'onlyPowerChanges' mode ignores thresholds
|
||||
if (mode === 'onlyPowerChanges') {
|
||||
logger.info('Shutdown action skipped: triggerMode is onlyPowerChanges, ignoring threshold');
|
||||
return false;
|
||||
}
|
||||
// Check if thresholds are actually exceeded
|
||||
return this.areThresholdsExceeded(context.batteryCapacity, context.batteryRuntime);
|
||||
}
|
||||
|
||||
// Handle power status changes
|
||||
if (context.triggerReason === 'powerStatusChange') {
|
||||
// 'onlyThresholds' mode ignores power status changes
|
||||
if (mode === 'onlyThresholds') {
|
||||
logger.info('Shutdown action skipped: triggerMode is onlyThresholds, ignoring power change');
|
||||
return false;
|
||||
}
|
||||
|
||||
const prev = context.previousPowerStatus;
|
||||
|
||||
// Only trigger on transitions TO onBattery from online (real power loss)
|
||||
if (prev === 'online') {
|
||||
logger.info('Shutdown action triggered: power loss detected (online → onBattery)');
|
||||
return true;
|
||||
}
|
||||
|
||||
// For unknown → onBattery (daemon started while on battery):
|
||||
// This is a startup scenario - be cautious. The user may have just started
|
||||
// the daemon for testing, or the UPS may have been on battery for a while.
|
||||
// Only trigger if mode explicitly includes power changes.
|
||||
if (prev === 'unknown') {
|
||||
if (mode === 'onlyPowerChanges' || mode === 'powerChangesAndThresholds' || mode === 'anyChange') {
|
||||
logger.info('Shutdown action triggered: UPS on battery at daemon startup (unknown → onBattery)');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Other transitions (e.g., onBattery → onBattery) should not trigger
|
||||
logger.info(`Shutdown action skipped: non-emergency transition (${prev} → ${context.powerStatus})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// For 'anyChange' mode, always execute (UPS is already confirmed on battery)
|
||||
if (mode === 'anyChange') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the shutdown action
|
||||
* @param context Action context with UPS state
|
||||
@@ -26,10 +102,10 @@ export class ShutdownAction extends Action {
|
||||
return;
|
||||
}
|
||||
|
||||
const shutdownDelay = this.config.shutdownDelay || 5; // Default 5 minutes
|
||||
const shutdownDelay = this.config.shutdownDelay || SHUTDOWN.DEFAULT_DELAY_MINUTES;
|
||||
|
||||
logger.log('');
|
||||
logger.logBoxTitle('Initiating System Shutdown', 60, 'error');
|
||||
logger.logBoxTitle('Initiating System Shutdown', UI.WIDE_BOX_WIDTH, 'error');
|
||||
logger.logBoxLine(`UPS: ${context.upsName} (${context.upsId})`);
|
||||
logger.logBoxLine(`Power Status: ${context.powerStatus}`);
|
||||
logger.logBoxLine(`Battery: ${context.batteryCapacity}%`);
|
||||
|
||||
@@ -3,6 +3,32 @@ import * as https from 'node:https';
|
||||
import { URL } from 'node:url';
|
||||
import { Action, type IActionConfig, type IActionContext } from './base-action.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
import { WEBHOOK } from '../constants.ts';
|
||||
|
||||
/**
|
||||
* Payload sent to webhook endpoints
|
||||
*/
|
||||
export interface IWebhookPayload {
|
||||
/** UPS ID */
|
||||
upsId: string;
|
||||
/** UPS name */
|
||||
upsName: string;
|
||||
/** Current power status */
|
||||
powerStatus: 'online' | 'onBattery' | 'unknown';
|
||||
/** Current battery capacity percentage */
|
||||
batteryCapacity: number;
|
||||
/** Current battery runtime in minutes */
|
||||
batteryRuntime: number;
|
||||
/** Reason this webhook was triggered */
|
||||
triggerReason: 'powerStatusChange' | 'thresholdViolation';
|
||||
/** Timestamp when webhook was triggered */
|
||||
timestamp: number;
|
||||
/** Thresholds configured for this action (if any) */
|
||||
thresholds?: {
|
||||
battery: number;
|
||||
runtime: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* WebhookAction - Calls an HTTP webhook with UPS state information
|
||||
@@ -30,7 +56,7 @@ export class WebhookAction extends Action {
|
||||
}
|
||||
|
||||
const method = this.config.webhookMethod || 'POST';
|
||||
const timeout = this.config.webhookTimeout || 10000;
|
||||
const timeout = this.config.webhookTimeout || WEBHOOK.DEFAULT_TIMEOUT_MS;
|
||||
|
||||
logger.info(`Calling webhook: ${method} ${this.config.webhookUrl}`);
|
||||
|
||||
@@ -56,7 +82,7 @@ export class WebhookAction extends Action {
|
||||
method: 'GET' | 'POST',
|
||||
timeout: number,
|
||||
): Promise<void> {
|
||||
const payload: any = {
|
||||
const payload: IWebhookPayload = {
|
||||
upsId: context.upsId,
|
||||
upsName: context.upsName,
|
||||
powerStatus: context.powerStatus,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { logger, type ITableColumn } from '../logger.ts';
|
||||
import { theme, symbols } from '../colors.ts';
|
||||
import type { IActionConfig } from '../actions/base-action.ts';
|
||||
import type { IUpsConfig, IGroupConfig } from '../daemon.ts';
|
||||
import * as helpers from '../helpers/index.ts';
|
||||
|
||||
/**
|
||||
* Class for handling action-related CLI commands
|
||||
@@ -57,21 +58,7 @@ export class ActionHandler {
|
||||
const targetType = ups ? 'UPS' : 'Group';
|
||||
const targetName = ups ? ups.name : group!.name;
|
||||
|
||||
const readline = await import('node:readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const prompt = (question: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer: string) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await helpers.withPrompt(async (prompt) => {
|
||||
logger.log('');
|
||||
logger.info(`Add Action to ${targetType} ${theme.highlight(targetName)}`);
|
||||
logger.log('');
|
||||
@@ -154,9 +141,7 @@ export class ActionHandler {
|
||||
logger.success(`Action added to ${targetType} ${targetName}`);
|
||||
logger.log(` ${theme.dim('Changes saved and will be applied automatically')}`);
|
||||
logger.log('');
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to add action: ${error instanceof Error ? error.message : String(error)}`,
|
||||
|
||||
@@ -25,26 +25,9 @@ export class FeatureHandler {
|
||||
*/
|
||||
public async configureHttpServer(): Promise<void> {
|
||||
try {
|
||||
const readline = await import('node:readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const prompt = (question: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer: string) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await helpers.withPrompt(async (prompt) => {
|
||||
await this.runHttpServerConfig(prompt);
|
||||
} finally {
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`HTTP Server config error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
@@ -186,17 +169,9 @@ export class FeatureHandler {
|
||||
|
||||
if (isActive) {
|
||||
logger.log('');
|
||||
const readline = await import('node:readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const answer = await new Promise<string>((resolve) => {
|
||||
rl.question('Service is running. Restart to apply changes? (Y/n): ', resolve);
|
||||
});
|
||||
|
||||
rl.close();
|
||||
const { prompt, close } = await helpers.createPrompt();
|
||||
const answer = await prompt('Service is running. Restart to apply changes? (Y/n): ');
|
||||
close();
|
||||
|
||||
if (!answer || answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
||||
logger.info('Restarting service...');
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Nupst } from '../nupst.ts';
|
||||
import { logger, type ITableColumn } from '../logger.ts';
|
||||
import { theme } from '../colors.ts';
|
||||
import * as helpers from '../helpers/index.ts';
|
||||
import { type IGroupConfig } from '../daemon.ts';
|
||||
import type { IGroupConfig, IUpsConfig, INupstConfig } from '../daemon.ts';
|
||||
|
||||
/**
|
||||
* Class for handling group-related CLI commands
|
||||
@@ -100,24 +100,7 @@ export class GroupHandler {
|
||||
*/
|
||||
public async add(): Promise<void> {
|
||||
try {
|
||||
// Import readline module for user input
|
||||
const readline = await import('node:readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
// Helper function to prompt for input
|
||||
const prompt = (question: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer: string) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await helpers.withPrompt(async (prompt) => {
|
||||
// Try to load configuration
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
@@ -200,10 +183,7 @@ export class GroupHandler {
|
||||
this.nupst.getUpsHandler().restartServiceIfRunning();
|
||||
|
||||
logger.log('\nGroup setup complete!');
|
||||
} finally {
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
@@ -215,24 +195,7 @@ export class GroupHandler {
|
||||
*/
|
||||
public async edit(groupId: string): Promise<void> {
|
||||
try {
|
||||
// Import readline module for user input
|
||||
const readline = await import('node:readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
// Helper function to prompt for input
|
||||
const prompt = (question: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer: string) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await helpers.withPrompt(async (prompt) => {
|
||||
// Try to load configuration
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
@@ -318,10 +281,7 @@ export class GroupHandler {
|
||||
this.nupst.getUpsHandler().restartServiceIfRunning();
|
||||
|
||||
logger.log('\nGroup edit complete!');
|
||||
} finally {
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
@@ -362,23 +322,11 @@ export class GroupHandler {
|
||||
const groupToDelete = config.groups[groupIndex];
|
||||
|
||||
// Get confirmation before deleting
|
||||
const readline = await import('node:readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const confirm = await new Promise<string>((resolve) => {
|
||||
rl.question(
|
||||
`Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `,
|
||||
(answer) => {
|
||||
resolve(answer.toLowerCase());
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
const { prompt, close } = await helpers.createPrompt();
|
||||
const confirm = (await prompt(
|
||||
`Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `,
|
||||
)).toLowerCase();
|
||||
close();
|
||||
|
||||
if (confirm !== 'y' && confirm !== 'yes') {
|
||||
logger.log('Deletion cancelled.');
|
||||
@@ -419,8 +367,8 @@ export class GroupHandler {
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
public async assignUpsToGroups(
|
||||
ups: any,
|
||||
groups: any[],
|
||||
ups: IUpsConfig,
|
||||
groups: IGroupConfig[],
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
// Initialize groups array if it doesn't exist
|
||||
@@ -514,7 +462,7 @@ export class GroupHandler {
|
||||
*/
|
||||
public async assignUpsToGroup(
|
||||
groupId: string,
|
||||
config: any,
|
||||
config: INupstConfig,
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
if (!config.upsDevices || config.upsDevices.length === 0) {
|
||||
@@ -522,7 +470,7 @@ export class GroupHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
const group = config.groups.find((g: { id: string }) => g.id === groupId);
|
||||
const group = config.groups.find((g) => g.id === groupId);
|
||||
if (!group) {
|
||||
logger.error(`Group with ID "${groupId}" not found.`);
|
||||
return;
|
||||
@@ -530,7 +478,7 @@ export class GroupHandler {
|
||||
|
||||
// Show current assignments
|
||||
logger.log(`\nUPS devices in group "${group.name}" (${group.id}):`);
|
||||
const upsInGroup = config.upsDevices.filter((ups: { groups?: string[] }) =>
|
||||
const upsInGroup = config.upsDevices.filter((ups) =>
|
||||
ups.groups && ups.groups.includes(groupId)
|
||||
);
|
||||
if (upsInGroup.length === 0) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import process from 'node:process';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { Nupst } from '../nupst.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
import * as helpers from '../helpers/index.ts';
|
||||
|
||||
/**
|
||||
* Class for handling service-related CLI commands
|
||||
@@ -196,22 +197,7 @@ export class ServiceHandler {
|
||||
this.checkRootAccess('This command must be run as root.');
|
||||
|
||||
try {
|
||||
// Import readline module for user input
|
||||
const readline = await import('readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
// Helper function to prompt for input
|
||||
const prompt = (question: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer: string) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
};
|
||||
const { prompt, close } = await helpers.createPrompt();
|
||||
|
||||
logger.log('');
|
||||
logger.highlight('NUPST Uninstaller');
|
||||
@@ -254,15 +240,13 @@ export class ServiceHandler {
|
||||
|
||||
if (!uninstallScriptPath) {
|
||||
logger.error('Could not locate uninstall.sh script. Aborting uninstall.');
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
close();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Close readline before executing script
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
// Close prompt before executing script
|
||||
close();
|
||||
|
||||
// Execute uninstall.sh with the appropriate option
|
||||
logger.log('');
|
||||
|
||||
@@ -4,8 +4,17 @@ import { Nupst } from '../nupst.ts';
|
||||
import { logger, type ITableColumn } from '../logger.ts';
|
||||
import { theme } from '../colors.ts';
|
||||
import * as helpers from '../helpers/index.ts';
|
||||
import type { TUpsModel } from '../snmp/types.ts';
|
||||
import type { INupstConfig } from '../daemon.ts';
|
||||
import type { ISnmpConfig, TUpsModel, IUpsStatus as ISnmpUpsStatus } from '../snmp/types.ts';
|
||||
import type { INupstConfig, IUpsConfig, IUpsStatus } from '../daemon.ts';
|
||||
import type { IActionConfig } from '../actions/base-action.ts';
|
||||
|
||||
/**
|
||||
* Thresholds configuration for CLI display
|
||||
*/
|
||||
interface IThresholds {
|
||||
battery: number;
|
||||
runtime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for handling UPS-related CLI commands
|
||||
@@ -27,29 +36,9 @@ export class UpsHandler {
|
||||
*/
|
||||
public async add(): Promise<void> {
|
||||
try {
|
||||
// Import readline module for user input
|
||||
const readline = await import('node:readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
// Helper function to prompt for input
|
||||
const prompt = (question: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer: string) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await helpers.withPrompt(async (prompt) => {
|
||||
await this.runAddProcess(prompt);
|
||||
} finally {
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Add UPS error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
@@ -160,29 +149,9 @@ export class UpsHandler {
|
||||
*/
|
||||
public async edit(upsId?: string): Promise<void> {
|
||||
try {
|
||||
// Import readline module for user input
|
||||
const readline = await import('node:readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
// Helper function to prompt for input
|
||||
const prompt = (question: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer: string) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await helpers.withPrompt(async (prompt) => {
|
||||
await this.runEditProcess(upsId, prompt);
|
||||
} finally {
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Edit UPS error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
@@ -337,23 +306,11 @@ export class UpsHandler {
|
||||
const upsToDelete = config.upsDevices[upsIndex];
|
||||
|
||||
// Get confirmation before deleting
|
||||
const readline = await import('node:readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const confirm = await new Promise<string>((resolve) => {
|
||||
rl.question(
|
||||
`Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `,
|
||||
(answer) => {
|
||||
resolve(answer.toLowerCase());
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
const { prompt, close } = await helpers.createPrompt();
|
||||
const confirm = (await prompt(
|
||||
`Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `,
|
||||
)).toLowerCase();
|
||||
close();
|
||||
|
||||
if (confirm !== 'y' && confirm !== 'yes') {
|
||||
logger.log('Deletion cancelled.');
|
||||
@@ -509,7 +466,7 @@ export class UpsHandler {
|
||||
* Display the configuration for testing
|
||||
* @param config Current configuration or individual UPS configuration
|
||||
*/
|
||||
private displayTestConfig(config: any): void {
|
||||
private displayTestConfig(config: IUpsConfig | INupstConfig): void {
|
||||
// Check if this is a UPS device or full configuration
|
||||
const isUpsConfig = config.snmp;
|
||||
const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {};
|
||||
@@ -571,7 +528,7 @@ export class UpsHandler {
|
||||
* Test connection to the UPS
|
||||
* @param config Current UPS configuration or legacy config
|
||||
*/
|
||||
private async testConnection(config: any): Promise<void> {
|
||||
private async testConnection(config: IUpsConfig | INupstConfig): Promise<void> {
|
||||
const upsId = config.id || 'default';
|
||||
const upsName = config.name || 'Default UPS';
|
||||
logger.log(`\nTesting connection to UPS: ${upsName} (${upsId})...`);
|
||||
@@ -610,7 +567,7 @@ export class UpsHandler {
|
||||
* @param status UPS status
|
||||
* @param thresholds Threshold configuration
|
||||
*/
|
||||
private analyzeThresholds(status: any, thresholds: any): void {
|
||||
private analyzeThresholds(status: ISnmpUpsStatus, thresholds: IThresholds): void {
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Threshold Analysis', boxWidth);
|
||||
|
||||
@@ -649,7 +606,7 @@ export class UpsHandler {
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
private async gatherSnmpSettings(
|
||||
snmpConfig: any,
|
||||
snmpConfig: Partial<ISnmpConfig>,
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
// SNMP IP Address
|
||||
@@ -693,7 +650,7 @@ export class UpsHandler {
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
private async gatherSnmpV3Settings(
|
||||
snmpConfig: any,
|
||||
snmpConfig: Partial<ISnmpConfig>,
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
logger.log('');
|
||||
@@ -771,7 +728,7 @@ export class UpsHandler {
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
private async gatherAuthenticationSettings(
|
||||
snmpConfig: any,
|
||||
snmpConfig: Partial<ISnmpConfig>,
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
// Authentication protocol
|
||||
@@ -798,7 +755,7 @@ export class UpsHandler {
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
private async gatherPrivacySettings(
|
||||
snmpConfig: any,
|
||||
snmpConfig: Partial<ISnmpConfig>,
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
// Privacy protocol
|
||||
@@ -823,7 +780,7 @@ export class UpsHandler {
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
private async gatherUpsModelSettings(
|
||||
snmpConfig: any,
|
||||
snmpConfig: Partial<ISnmpConfig>,
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
logger.log('');
|
||||
@@ -888,7 +845,7 @@ export class UpsHandler {
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
private async gatherActionSettings(
|
||||
actions: any[],
|
||||
actions: IActionConfig[],
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
logger.log('');
|
||||
@@ -915,7 +872,7 @@ export class UpsHandler {
|
||||
const typeInput = await prompt('Select action type [1]: ');
|
||||
const typeValue = parseInt(typeInput, 10) || 1;
|
||||
|
||||
const action: any = {};
|
||||
const action: Partial<IActionConfig> = {};
|
||||
|
||||
if (typeValue === 1) {
|
||||
// Shutdown action
|
||||
@@ -1014,8 +971,8 @@ export class UpsHandler {
|
||||
};
|
||||
}
|
||||
|
||||
actions.push(action);
|
||||
logger.success(`${action.type.charAt(0).toUpperCase() + action.type.slice(1)} action added (mode: ${action.triggerMode || 'powerChangesAndThresholds'})`);
|
||||
actions.push(action as IActionConfig);
|
||||
logger.success(`${action.type!.charAt(0).toUpperCase() + action.type!.slice(1)} action added (mode: ${action.triggerMode || 'powerChangesAndThresholds'})`);
|
||||
|
||||
const more = await prompt('Add another action? (y/N): ');
|
||||
addMore = more.toLowerCase() === 'y';
|
||||
@@ -1031,7 +988,7 @@ export class UpsHandler {
|
||||
* Display UPS configuration summary
|
||||
* @param ups UPS configuration
|
||||
*/
|
||||
private displayUpsConfigSummary(ups: any): void {
|
||||
private displayUpsConfigSummary(ups: IUpsConfig): void {
|
||||
const boxWidth = 45;
|
||||
logger.log('');
|
||||
logger.logBoxTitle(`UPS Configuration: ${ups.name}`, boxWidth);
|
||||
@@ -1055,7 +1012,7 @@ export class UpsHandler {
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
private async optionallyTestConnection(
|
||||
snmpConfig: any,
|
||||
snmpConfig: ISnmpConfig,
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
const testConnection = await prompt(
|
||||
|
||||
118
ts/constants.ts
Normal file
118
ts/constants.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* NUPST Constants
|
||||
*
|
||||
* Central location for all timeout, interval, and threshold values.
|
||||
* This makes configuration easier and code more self-documenting.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default timing values in milliseconds
|
||||
*/
|
||||
export const TIMING = {
|
||||
/** Default interval between UPS status checks (30 seconds) */
|
||||
CHECK_INTERVAL_MS: 30000,
|
||||
|
||||
/** Interval for idle monitoring mode (60 seconds) */
|
||||
IDLE_CHECK_INTERVAL_MS: 60000,
|
||||
|
||||
/** Interval for checking config file changes (60 seconds) */
|
||||
CONFIG_CHECK_INTERVAL_MS: 60000,
|
||||
|
||||
/** Interval for logging periodic status updates (5 minutes) */
|
||||
LOG_INTERVAL_MS: 5 * 60 * 1000,
|
||||
|
||||
/** Maximum time to monitor during shutdown (5 minutes) */
|
||||
MAX_SHUTDOWN_MONITORING_MS: 5 * 60 * 1000,
|
||||
|
||||
/** Interval for UPS checks during shutdown (30 seconds) */
|
||||
SHUTDOWN_CHECK_INTERVAL_MS: 30000,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* SNMP-related constants
|
||||
*/
|
||||
export const SNMP = {
|
||||
/** Default SNMP port */
|
||||
DEFAULT_PORT: 161,
|
||||
|
||||
/** Default SNMP timeout (5 seconds) */
|
||||
DEFAULT_TIMEOUT_MS: 5000,
|
||||
|
||||
/** Number of SNMP retries */
|
||||
RETRIES: 2,
|
||||
|
||||
/** Timeout for noAuthNoPriv security level (5 seconds) */
|
||||
TIMEOUT_NO_AUTH_MS: 5000,
|
||||
|
||||
/** Timeout for authNoPriv security level (10 seconds) */
|
||||
TIMEOUT_AUTH_MS: 10000,
|
||||
|
||||
/** Timeout for authPriv security level (15 seconds) */
|
||||
TIMEOUT_AUTH_PRIV_MS: 15000,
|
||||
|
||||
/** Maximum timeout for connection tests (10 seconds) */
|
||||
MAX_TEST_TIMEOUT_MS: 10000,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Default threshold values
|
||||
*/
|
||||
export const THRESHOLDS = {
|
||||
/** Default battery capacity threshold for shutdown (60%) */
|
||||
DEFAULT_BATTERY_PERCENT: 60,
|
||||
|
||||
/** Default runtime threshold for shutdown (20 minutes) */
|
||||
DEFAULT_RUNTIME_MINUTES: 20,
|
||||
|
||||
/** Emergency runtime threshold during shutdown (5 minutes) */
|
||||
EMERGENCY_RUNTIME_MINUTES: 5,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Webhook action constants
|
||||
*/
|
||||
export const WEBHOOK = {
|
||||
/** Default webhook request timeout (10 seconds) */
|
||||
DEFAULT_TIMEOUT_MS: 10000,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Script action constants
|
||||
*/
|
||||
export const SCRIPT = {
|
||||
/** Default script execution timeout (60 seconds) */
|
||||
DEFAULT_TIMEOUT_MS: 60000,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Shutdown action constants
|
||||
*/
|
||||
export const SHUTDOWN = {
|
||||
/** Default shutdown delay (5 minutes) */
|
||||
DEFAULT_DELAY_MINUTES: 5,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* HTTP Server constants
|
||||
*/
|
||||
export const HTTP_SERVER = {
|
||||
/** Default HTTP server port */
|
||||
DEFAULT_PORT: 8080,
|
||||
|
||||
/** Default URL path for UPS status endpoint */
|
||||
DEFAULT_PATH: '/ups-status',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* UI/Display constants
|
||||
*/
|
||||
export const UI = {
|
||||
/** Default width for log boxes */
|
||||
DEFAULT_BOX_WIDTH: 45,
|
||||
|
||||
/** Wide box width for status displays */
|
||||
WIDE_BOX_WIDTH: 60,
|
||||
|
||||
/** Extra wide box width for detailed info */
|
||||
EXTRA_WIDE_BOX_WIDTH: 70,
|
||||
} as const;
|
||||
70
ts/daemon.ts
70
ts/daemon.ts
@@ -11,6 +11,7 @@ import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } f
|
||||
import type { IActionConfig } from './actions/base-action.ts';
|
||||
import { ActionManager, type IActionContext, type TPowerStatus } from './actions/index.ts';
|
||||
import { NupstHttpServer } from './http-server.ts';
|
||||
import { TIMING, THRESHOLDS, UI } from './constants.ts';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -144,8 +145,8 @@ export class NupstDaemon {
|
||||
type: 'shutdown',
|
||||
triggerMode: 'onlyThresholds',
|
||||
thresholds: {
|
||||
battery: 60, // Shutdown when battery below 60%
|
||||
runtime: 20, // Shutdown when runtime below 20 minutes
|
||||
battery: THRESHOLDS.DEFAULT_BATTERY_PERCENT, // Shutdown when battery below 60%
|
||||
runtime: THRESHOLDS.DEFAULT_RUNTIME_MINUTES, // Shutdown when runtime below 20 minutes
|
||||
},
|
||||
shutdownDelay: 5,
|
||||
},
|
||||
@@ -153,7 +154,7 @@ export class NupstDaemon {
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
checkInterval: 30000, // Check every 30 seconds
|
||||
checkInterval: TIMING.CHECK_INTERVAL_MS, // Check every 30 seconds
|
||||
}
|
||||
|
||||
private config: INupstConfig;
|
||||
@@ -282,20 +283,23 @@ export class NupstDaemon {
|
||||
this.logConfigLoaded();
|
||||
|
||||
// Log version information
|
||||
this.snmp.getNupst().logVersionInfo(false); // Don't check for updates immediately on startup
|
||||
const nupst = this.snmp.getNupst();
|
||||
if (nupst) {
|
||||
nupst.logVersionInfo(false); // Don't check for updates immediately on startup
|
||||
|
||||
// Check for updates in the background
|
||||
this.snmp.getNupst().checkForUpdates().then((updateAvailable: boolean) => {
|
||||
if (updateAvailable) {
|
||||
const updateStatus = this.snmp.getNupst().getUpdateStatus();
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Update Available', boxWidth);
|
||||
logger.logBoxLine(`Current Version: ${updateStatus.currentVersion}`);
|
||||
logger.logBoxLine(`Latest Version: ${updateStatus.latestVersion}`);
|
||||
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}).catch(() => {}); // Ignore errors checking for updates
|
||||
// Check for updates in the background
|
||||
nupst.checkForUpdates().then((updateAvailable: boolean) => {
|
||||
if (updateAvailable) {
|
||||
const updateStatus = nupst.getUpdateStatus();
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Update Available', boxWidth);
|
||||
logger.logBoxLine(`Current Version: ${updateStatus.currentVersion}`);
|
||||
logger.logBoxLine(`Latest Version: ${updateStatus.latestVersion}`);
|
||||
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}).catch(() => {}); // Ignore errors checking for updates
|
||||
}
|
||||
|
||||
// Initialize UPS status tracking
|
||||
this.initializeUpsStatus();
|
||||
@@ -441,7 +445,6 @@ export class NupstDaemon {
|
||||
}
|
||||
|
||||
let lastLogTime = 0; // Track when we last logged status
|
||||
const LOG_INTERVAL = 5 * 60 * 1000; // Log at least every 5 minutes (300000ms)
|
||||
|
||||
// Monitor continuously
|
||||
while (this.isRunning) {
|
||||
@@ -451,7 +454,7 @@ export class NupstDaemon {
|
||||
|
||||
// Log periodic status update
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastLogTime >= LOG_INTERVAL) {
|
||||
if (currentTime - lastLogTime >= TIMING.LOG_INTERVAL_MS) {
|
||||
this.logAllUpsStatus();
|
||||
lastLogTime = currentTime;
|
||||
}
|
||||
@@ -789,21 +792,18 @@ export class NupstDaemon {
|
||||
* Force immediate shutdown if any UPS gets critically low
|
||||
*/
|
||||
private async monitorDuringShutdown(): Promise<void> {
|
||||
const EMERGENCY_RUNTIME_THRESHOLD = 5; // 5 minutes remaining is critical
|
||||
const CHECK_INTERVAL = 30000; // Check every 30 seconds during shutdown
|
||||
const MAX_MONITORING_TIME = 5 * 60 * 1000; // Max 5 minutes of monitoring
|
||||
const startTime = Date.now();
|
||||
|
||||
logger.log('');
|
||||
logger.logBoxTitle('Shutdown Monitoring Active', 60, 'warning');
|
||||
logger.logBoxLine(`Emergency threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes runtime`);
|
||||
logger.logBoxLine(`Check interval: ${CHECK_INTERVAL / 1000} seconds`);
|
||||
logger.logBoxLine(`Max monitoring time: ${MAX_MONITORING_TIME / 1000} seconds`);
|
||||
logger.logBoxTitle('Shutdown Monitoring Active', UI.WIDE_BOX_WIDTH, 'warning');
|
||||
logger.logBoxLine(`Emergency threshold: ${THRESHOLDS.EMERGENCY_RUNTIME_MINUTES} minutes runtime`);
|
||||
logger.logBoxLine(`Check interval: ${TIMING.SHUTDOWN_CHECK_INTERVAL_MS / 1000} seconds`);
|
||||
logger.logBoxLine(`Max monitoring time: ${TIMING.MAX_SHUTDOWN_MONITORING_MS / 1000} seconds`);
|
||||
logger.logBoxEnd();
|
||||
logger.log('');
|
||||
|
||||
// Continue monitoring until max monitoring time is reached
|
||||
while (Date.now() - startTime < MAX_MONITORING_TIME) {
|
||||
while (Date.now() - startTime < TIMING.MAX_SHUTDOWN_MONITORING_MS) {
|
||||
try {
|
||||
logger.info('Checking UPS status during shutdown...');
|
||||
|
||||
@@ -827,7 +827,7 @@ export class NupstDaemon {
|
||||
const batteryColor = getBatteryColor(status.batteryCapacity);
|
||||
const runtimeColor = getRuntimeColor(status.batteryRuntime);
|
||||
|
||||
const isCritical = status.batteryRuntime < EMERGENCY_RUNTIME_THRESHOLD;
|
||||
const isCritical = status.batteryRuntime < THRESHOLDS.EMERGENCY_RUNTIME_MINUTES;
|
||||
|
||||
rows.push({
|
||||
name: ups.name,
|
||||
@@ -868,7 +868,7 @@ export class NupstDaemon {
|
||||
logger.logBoxLine(
|
||||
`UPS ${emergencyUps.ups.name} runtime critically low: ${emergencyUps.status.batteryRuntime} minutes`,
|
||||
);
|
||||
logger.logBoxLine(`Emergency threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes`);
|
||||
logger.logBoxLine(`Emergency threshold: ${THRESHOLDS.EMERGENCY_RUNTIME_MINUTES} minutes`);
|
||||
logger.logBoxLine('Forcing immediate shutdown!');
|
||||
logger.logBoxEnd();
|
||||
logger.log('');
|
||||
@@ -879,14 +879,14 @@ export class NupstDaemon {
|
||||
}
|
||||
|
||||
// Wait before checking again
|
||||
await this.sleep(CHECK_INTERVAL);
|
||||
await this.sleep(TIMING.SHUTDOWN_CHECK_INTERVAL_MS);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error monitoring UPS during shutdown: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
await this.sleep(CHECK_INTERVAL);
|
||||
await this.sleep(TIMING.SHUTDOWN_CHECK_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -988,12 +988,10 @@ export class NupstDaemon {
|
||||
* Watches for config changes and reloads when detected
|
||||
*/
|
||||
private async idleMonitoring(): Promise<void> {
|
||||
const IDLE_CHECK_INTERVAL = 60000; // Check every 60 seconds
|
||||
let lastConfigCheck = Date.now();
|
||||
const CONFIG_CHECK_INTERVAL = 60000; // Check config every minute
|
||||
|
||||
logger.log('Entering idle monitoring mode...');
|
||||
logger.log('Daemon will check for config changes every 60 seconds');
|
||||
logger.log(`Daemon will check for config changes every ${TIMING.IDLE_CHECK_INTERVAL_MS / 1000} seconds`);
|
||||
|
||||
// Start file watcher for hot-reload
|
||||
this.watchConfigFile();
|
||||
@@ -1003,7 +1001,7 @@ export class NupstDaemon {
|
||||
const currentTime = Date.now();
|
||||
|
||||
// Periodically check if config has been updated
|
||||
if (currentTime - lastConfigCheck >= CONFIG_CHECK_INTERVAL) {
|
||||
if (currentTime - lastConfigCheck >= TIMING.CONFIG_CHECK_INTERVAL_MS) {
|
||||
try {
|
||||
// Try to load config
|
||||
const newConfig = await this.loadConfig();
|
||||
@@ -1023,12 +1021,12 @@ export class NupstDaemon {
|
||||
lastConfigCheck = currentTime;
|
||||
}
|
||||
|
||||
await this.sleep(IDLE_CHECK_INTERVAL);
|
||||
await this.sleep(TIMING.IDLE_CHECK_INTERVAL_MS);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error during idle monitoring: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
await this.sleep(IDLE_CHECK_INTERVAL);
|
||||
await this.sleep(TIMING.IDLE_CHECK_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './shortid.ts';
|
||||
export * from './prompt.ts';
|
||||
|
||||
55
ts/helpers/prompt.ts
Normal file
55
ts/helpers/prompt.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import process from 'node:process';
|
||||
|
||||
/**
|
||||
* Result from creating a prompt interface
|
||||
*/
|
||||
export interface IPromptInterface {
|
||||
/** Function to prompt for user input */
|
||||
prompt: (question: string) => Promise<string>;
|
||||
/** Function to close the prompt interface */
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a readline prompt interface for interactive CLI input
|
||||
* @returns Promise resolving to prompt function and close function
|
||||
*/
|
||||
export async function createPrompt(): Promise<IPromptInterface> {
|
||||
const readline = await import('node:readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const prompt = (question: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer: string) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const close = (): void => {
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
};
|
||||
|
||||
return { prompt, close };
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an async function with a prompt interface, ensuring cleanup
|
||||
* @param fn Function to run with the prompt interface
|
||||
* @returns Promise resolving to the function's return value
|
||||
*/
|
||||
export async function withPrompt<T>(
|
||||
fn: (prompt: (question: string) => Promise<string>) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const { prompt, close } = await createPrompt();
|
||||
try {
|
||||
return await fn(prompt);
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
}
|
||||
1
ts/interfaces/index.ts
Normal file
1
ts/interfaces/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './nupst-accessor.ts';
|
||||
41
ts/interfaces/nupst-accessor.ts
Normal file
41
ts/interfaces/nupst-accessor.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { NupstDaemon } from '../daemon.ts';
|
||||
|
||||
/**
|
||||
* Update status information
|
||||
*/
|
||||
export interface IUpdateStatus {
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
updateAvailable: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for accessing Nupst functionality from SNMP manager
|
||||
* This breaks the circular dependency between Nupst and NupstSnmp
|
||||
*/
|
||||
export interface INupstAccessor {
|
||||
/**
|
||||
* Get the daemon manager for background monitoring
|
||||
*/
|
||||
getDaemon(): NupstDaemon;
|
||||
|
||||
/**
|
||||
* Get the current version of NUPST
|
||||
*/
|
||||
getVersion(): string;
|
||||
|
||||
/**
|
||||
* Check if an update is available
|
||||
*/
|
||||
checkForUpdates(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get update status information
|
||||
*/
|
||||
getUpdateStatus(): IUpdateStatus;
|
||||
|
||||
/**
|
||||
* Log the current version and update status
|
||||
*/
|
||||
logVersionInfo(checkForUpdates?: boolean): void;
|
||||
}
|
||||
@@ -9,12 +9,13 @@ import { ServiceHandler } from './cli/service-handler.ts';
|
||||
import { ActionHandler } from './cli/action-handler.ts';
|
||||
import { FeatureHandler } from './cli/feature-handler.ts';
|
||||
import * as https from 'node:https';
|
||||
import type { INupstAccessor, IUpdateStatus } from './interfaces/index.ts';
|
||||
|
||||
/**
|
||||
* Main Nupst class that coordinates all components
|
||||
* Acts as a facade to access SNMP, Daemon, and Systemd functionality
|
||||
*/
|
||||
export class Nupst {
|
||||
export class Nupst implements INupstAccessor {
|
||||
private readonly snmp: NupstSnmp;
|
||||
private readonly daemon: NupstDaemon;
|
||||
private readonly systemd: NupstSystemd;
|
||||
@@ -134,11 +135,7 @@ export class Nupst {
|
||||
* Get update status information
|
||||
* @returns Object with update status information
|
||||
*/
|
||||
public getUpdateStatus(): {
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
updateAvailable: boolean;
|
||||
} {
|
||||
public getUpdateStatus(): IUpdateStatus {
|
||||
return {
|
||||
currentVersion: this.getVersion(),
|
||||
latestVersion: this.latestVersion || this.getVersion(),
|
||||
|
||||
@@ -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