Compare commits

..

9 Commits

Author SHA1 Message Date
7d595fa175 chore(release): bump version to 4.1.3
All checks were successful
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 4s
Release / build-and-release (push) Successful in 45s
CI / Build All Platforms (push) Successful in 51s
2025-10-20 00:40:56 +00:00
df417432b0 chore(branding): update description to 'Network UPS Shutdown Tool' 2025-10-20 00:40:52 +00:00
e5f1ebf343 chore(release): bump version to 4.1.2
All checks were successful
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 46s
CI / Build All Platforms (push) Successful in 50s
2025-10-20 00:34:03 +00:00
3ff0dd7ac8 fix(cli): resolve process hang and improve output consistency
- Add process.stdin.destroy() after rl.close() in all interactive commands
  to properly release stdin and allow process to exit cleanly
- Replace raw console.log with logger methods throughout CLI handlers
- Convert manual box drawing to logger.logBox() in daemon.ts
- Standardize menu formatting with logger.info() and logger.dim()
- Improve migration output to only show when migrations actually run

Fixes issue where process would not exit after "Setup complete!" message
due to stdin keeping the event loop alive.
2025-10-20 00:32:06 +00:00
bb87316dd3 fix(snmp): correct power status interpretation using OID set mappings
All checks were successful
CI / Type Check & Lint (push) Successful in 7s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 44s
CI / Build All Platforms (push) Successful in 49s
Move power status value interpretation from hardcoded logic to OID set configuration.
Each UPS model now defines its own value mappings (e.g., CyberPower: 2=online, 3=onBattery).

Fixes incorrect status display where UPS showed "On Battery" when actually online.

Changes:
- Add POWER_STATUS_VALUES to IOidSet interface
- Define value mappings for all UPS models (cyberpower, apc, eaton, tripplite, liebert)
- Refactor determinePowerStatus() to use OID set mappings instead of hardcoded values
- CyberPower now correctly interprets value 2 as online (was incorrectly onBattery)
2025-10-19 23:48:13 +00:00
d6e0a1a274 feat(cli): remove ALL ugly boxes from status output - now fully beautiful
All checks were successful
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 44s
CI / Build All Platforms (push) Successful in 49s
Removed the last remaining ugly ASCII boxes:
- Version info box (┌─┐│└┘) that appeared at top
- Async version check box that ended randomly in middle
- Configuration error box

Now status output is 100% clean and beautiful with just colored text:

● Service: active (running)
  PID: 9120  Memory: 45.7M  CPU: 190ms

UPS Devices (2):
  ⚠ Test UPS (SNMP v1) - On Battery
    Battery: 100% ✓  Runtime: 48 min
    Host: 192.168.187.140:161

  ◯ Test UPS (SNMP v3) - Unknown
    Battery: 0% ⚠  Runtime: 0 min
    Host: 192.168.187.140:161

No boxes, just beautiful colored output with symbols!
Bumped to v4.1.0 to mark completion of beautiful CLI feature.
2025-10-19 23:01:25 +00:00
95fa4f8b0b fix(update): normalize version strings for correct comparison
All checks were successful
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 46s
CI / Build All Platforms (push) Successful in 50s
The version check was comparing "4.0.8" (no prefix) with "v4.0.8"
(with prefix), causing it to always think an update was available.

Now both versions are normalized to have the "v" prefix before
comparison, so "Already up to date!" works correctly.
2025-10-19 22:56:35 +00:00
c2f2f1e2ee feat(update): add version check to skip update when already latest
All checks were successful
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 6s
Release / build-and-release (push) Successful in 48s
CI / Build All Platforms (push) Successful in 53s
Now `nupst update` checks current version against latest release before
downloading anything.

Behavior:
- Fetches latest version from Gitea API
- Compares with current version
- Shows "Already up to date!" if versions match
- Only downloads/installs if newer version available

Example output when up to date:
  Checking for updates...
  Current version: v4.0.8
  Latest version:  v4.0.8

  ✓ Already up to date!
2025-10-19 22:50:03 +00:00
936f86c346 fix(update): rewrite nupst update for v4 (download install script instead of git pull)
All checks were successful
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 6s
Release / build-and-release (push) Successful in 45s
CI / Build All Platforms (push) Successful in 50s
The update command was still using v3 logic (git pull, setup.sh) which
doesn't work for v4 binary distribution.

Now it simply:
1. Downloads install.sh from main branch
2. Runs it (handles download, stop, replace, restart automatically)

This is much simpler and matches how v4 is distributed. No more git,
no more setup.sh, just download the latest binary.
2025-10-19 21:54:05 +00:00
12 changed files with 195 additions and 189 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@serve.zone/nupst",
"version": "4.0.6",
"version": "4.1.3",
"exports": "./mod.ts",
"tasks": {
"dev": "deno run --allow-all mod.ts",

View File

@@ -6,5 +6,5 @@ import denoConfig from '../deno.json' with { type: 'json' };
export const commitinfo = {
name: denoConfig.name,
version: denoConfig.version,
description: 'Deno-powered UPS monitoring tool for SNMP-enabled UPS devices',
description: 'Network UPS Shutdown Tool (https://nupst.serve.zone)',
};

View File

@@ -469,7 +469,7 @@ export class NupstCli {
private showVersion(): void {
const version = this.nupst.getVersion();
logger.log(`NUPST version ${version}`);
logger.log('Deno-powered UPS monitoring tool');
logger.log('Network UPS Shutdown Tool (https://nupst.serve.zone)');
}
/**

View File

@@ -192,6 +192,7 @@ export class GroupHandler {
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)}`);
@@ -309,6 +310,7 @@ export class GroupHandler {
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)}`);
@@ -366,6 +368,7 @@ export class GroupHandler {
});
rl.close();
process.stdin.destroy();
if (confirm !== 'y' && confirm !== 'yes') {
logger.log('Deletion cancelled.');

View File

@@ -129,81 +129,57 @@ export class ServiceHandler {
try {
// Check if running as root
this.checkRootAccess(
'This command must be run as root to update NUPST and refresh the systemd service.',
'This command must be run as root to update NUPST.',
);
const boxWidth = 45;
logger.logBoxTitle('NUPST Update Process', boxWidth);
logger.logBoxLine('Updating NUPST from repository...');
// Determine the installation directory (assuming it's either /opt/nupst or the current directory)
const { existsSync } = await import('fs');
let installDir = '/opt/nupst';
if (!existsSync(installDir)) {
// If not installed in /opt/nupst, use the current directory
const { dirname } = await import('path');
installDir = dirname(dirname(process.argv[1])); // Go up two levels from the executable
logger.logBoxLine(`Using local installation directory: ${installDir}`);
}
console.log('');
logger.info('Checking for updates...');
try {
// 1. Update the repository
logger.logBoxLine('Pulling latest changes from git repository...');
execSync(`cd ${installDir} && git fetch origin && git reset --hard origin/main`, {
stdio: 'pipe',
// Get current version
const currentVersion = this.nupst.getVersion();
// Fetch latest version from Gitea API
const apiUrl = 'https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/latest';
const response = execSync(`curl -sSL ${apiUrl}`).toString();
const release = JSON.parse(response);
const latestVersion = release.tag_name; // e.g., "v4.0.7"
// Normalize versions for comparison (ensure both have "v" prefix)
const normalizedCurrent = currentVersion.startsWith('v') ? currentVersion : `v${currentVersion}`;
const normalizedLatest = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`;
logger.dim(`Current version: ${normalizedCurrent}`);
logger.dim(`Latest version: ${normalizedLatest}`);
console.log('');
// Compare normalized versions
if (normalizedCurrent === normalizedLatest) {
logger.success('Already up to date!');
console.log('');
return;
}
logger.info(`New version available: ${latestVersion}`);
logger.dim('Downloading and installing...');
console.log('');
// Download and run the install script
// This handles everything: download binary, stop service, replace, restart
const installUrl = 'https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh';
execSync(`curl -sSL ${installUrl} | bash`, {
stdio: 'inherit', // Show install script output to user
});
// 2. Run the install.sh script
logger.logBoxLine('Running install.sh to update NUPST...');
execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' });
// 3. Run the setup.sh script with force flag to update Node.js and dependencies
logger.logBoxLine('Running setup.sh to update Node.js and dependencies...');
execSync(`cd ${installDir} && bash ./setup.sh --force`, { stdio: 'pipe' });
// 4. Refresh the systemd service
logger.logBoxLine('Refreshing systemd service...');
// First check if service exists
let serviceExists = false;
try {
const output = execSync('systemctl list-unit-files | grep nupst.service').toString();
serviceExists = output.includes('nupst.service');
} catch (error) {
// If grep fails (service not found), serviceExists remains false
serviceExists = false;
}
if (serviceExists) {
// Stop the service if it's running
const isRunning =
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
if (isRunning) {
logger.logBoxLine('Stopping nupst service...');
execSync('systemctl stop nupst.service');
}
// Reinstall the service
logger.logBoxLine('Reinstalling systemd service...');
await this.nupst.getSystemd().install();
// Restart the service if it was running
if (isRunning) {
logger.logBoxLine('Restarting nupst service...');
execSync('systemctl start nupst.service');
}
} else {
logger.logBoxLine('Systemd service not installed, skipping service refresh.');
logger.logBoxLine('Run "nupst enable" to install the service.');
}
logger.logBoxLine('Update completed successfully!');
logger.logBoxEnd();
console.log('');
logger.success(`Updated to ${latestVersion}`);
console.log('');
} catch (error) {
logger.logBoxLine('Error during update process:');
logger.logBoxLine(`${error instanceof Error ? error.message : String(error)}`);
logger.logBoxEnd();
console.log('');
logger.error('Update failed');
logger.dim(`${error instanceof Error ? error.message : String(error)}`);
console.log('');
process.exit(1);
}
} catch (error) {
@@ -237,9 +213,11 @@ export class ServiceHandler {
});
};
console.log('\nNUPST Uninstaller');
console.log('===============');
console.log('This will completely remove NUPST from your system.\n');
logger.log('');
logger.highlight('NUPST Uninstaller');
logger.dim('===============');
logger.log('This will completely remove NUPST from your system.');
logger.log('');
// Ask about removing configuration
const removeConfig = await prompt(
@@ -275,17 +253,20 @@ export class ServiceHandler {
}
if (!uninstallScriptPath) {
console.error('Could not locate uninstall.sh script. Aborting uninstall.');
logger.error('Could not locate uninstall.sh script. Aborting uninstall.');
rl.close();
process.stdin.destroy();
process.exit(1);
}
}
// Close readline before executing script
rl.close();
process.stdin.destroy();
// Execute uninstall.sh with the appropriate option
console.log(`\nRunning uninstaller from ${uninstallScriptPath}...`);
logger.log('');
logger.log(`Running uninstaller from ${uninstallScriptPath}...`);
// Pass the configuration removal option as an environment variable
const env = {
@@ -301,7 +282,7 @@ export class ServiceHandler {
stdio: 'inherit', // Show output in the terminal
});
} catch (error) {
console.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
logger.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}

View File

@@ -47,6 +47,7 @@ export class UpsHandler {
await this.runAddProcess(prompt);
} finally {
rl.close();
process.stdin.destroy();
}
} catch (error) {
logger.error(`Add UPS error: ${error instanceof Error ? error.message : String(error)}`);
@@ -178,6 +179,7 @@ export class UpsHandler {
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)}`);
@@ -344,6 +346,7 @@ export class UpsHandler {
});
rl.close();
process.stdin.destroy();
if (confirm !== 'y' && confirm !== 'yes') {
logger.log('Deletion cancelled.');
@@ -667,10 +670,11 @@ export class UpsHandler {
// SNMP Version
const defaultVersion = snmpConfig.version || 1;
console.log('\nSNMP Version:');
console.log(' 1) SNMPv1');
console.log(' 2) SNMPv2c');
console.log(' 3) SNMPv3 (with security features)');
logger.log('');
logger.info('SNMP Version:');
logger.dim(' 1) SNMPv1');
logger.dim(' 2) SNMPv2c');
logger.dim(' 3) SNMPv3 (with security features)');
const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `);
const version = parseInt(versionInput, 10);
snmpConfig.version = versionInput.trim() && (version === 1 || version === 2 || version === 3)
@@ -697,13 +701,15 @@ export class UpsHandler {
snmpConfig: any,
prompt: (question: string) => Promise<string>,
): Promise<void> {
console.log('\nSNMPv3 Security Settings:');
logger.log('');
logger.info('SNMPv3 Security Settings:');
// Security Level
console.log('\nSecurity Level:');
console.log(' 1) noAuthNoPriv (No Authentication, No Privacy)');
console.log(' 2) authNoPriv (Authentication, No Privacy)');
console.log(' 3) authPriv (Authentication and Privacy)');
logger.log('');
logger.info('Security Level:');
logger.dim(' 1) noAuthNoPriv (No Authentication, No Privacy)');
logger.dim(' 2) authNoPriv (Authentication, No Privacy)');
logger.dim(' 3) authPriv (Authentication and Privacy)');
const defaultSecLevel = snmpConfig.securityLevel
? snmpConfig.securityLevel === 'noAuthNoPriv'
? 1
@@ -752,8 +758,9 @@ export class UpsHandler {
// Allow customizing the timeout value
const defaultTimeout = snmpConfig.timeout / 1000; // Convert from ms to seconds for display
console.log(
'\nSNMPv3 operations with authentication and privacy may require longer timeouts.',
logger.log('');
logger.info(
'SNMPv3 operations with authentication and privacy may require longer timeouts.',
);
const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `);
const timeout = parseInt(timeoutInput, 10);
@@ -773,9 +780,10 @@ export class UpsHandler {
prompt: (question: string) => Promise<string>,
): Promise<void> {
// Authentication protocol
console.log('\nAuthentication Protocol:');
console.log(' 1) MD5');
console.log(' 2) SHA');
logger.log('');
logger.info('Authentication Protocol:');
logger.dim(' 1) MD5');
logger.dim(' 2) SHA');
const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1;
const authProtocolInput = await prompt(
`Select Authentication Protocol [${defaultAuthProtocol}]: `,
@@ -799,9 +807,10 @@ export class UpsHandler {
prompt: (question: string) => Promise<string>,
): Promise<void> {
// Privacy protocol
console.log('\nPrivacy Protocol:');
console.log(' 1) DES');
console.log(' 2) AES');
logger.log('');
logger.info('Privacy Protocol:');
logger.dim(' 1) DES');
logger.dim(' 2) AES');
const defaultPrivProtocol = snmpConfig.privProtocol === 'AES' ? 2 : 1;
const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `);
const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol;
@@ -822,7 +831,8 @@ export class UpsHandler {
thresholds: any,
prompt: (question: string) => Promise<string>,
): Promise<void> {
console.log('\nShutdown Thresholds:');
logger.log('');
logger.info('Shutdown Thresholds:');
// Battery threshold
const defaultBatteryThreshold = thresholds.battery || 60;
@@ -854,13 +864,14 @@ export class UpsHandler {
snmpConfig: any,
prompt: (question: string) => Promise<string>,
): Promise<void> {
console.log('\nUPS Model Selection:');
console.log(' 1) CyberPower');
console.log(' 2) APC');
console.log(' 3) Eaton');
console.log(' 4) TrippLite');
console.log(' 5) Liebert/Vertiv');
console.log(' 6) Custom (Advanced)');
logger.log('');
logger.info('UPS Model Selection:');
logger.dim(' 1) CyberPower');
logger.dim(' 2) APC');
logger.dim(' 3) Eaton');
logger.dim(' 4) TrippLite');
logger.dim(' 5) Liebert/Vertiv');
logger.dim(' 6) Custom (Advanced)');
const defaultModelValue = snmpConfig.upsModel === 'cyberpower'
? 1
@@ -891,8 +902,9 @@ export class UpsHandler {
snmpConfig.upsModel = 'liebert';
} else if (modelValue === 6) {
snmpConfig.upsModel = 'custom';
console.log('\nEnter custom OIDs for your UPS:');
console.log('(Leave blank to use standard RFC 1628 OIDs as fallback)');
logger.log('');
logger.info('Enter custom OIDs for your UPS:');
logger.dim('(Leave blank to use standard RFC 1628 OIDs as fallback)');
// Custom OIDs
const powerStatusOID = await prompt('Power Status OID: ');

View File

@@ -207,11 +207,9 @@ export class NupstDaemon {
fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(configToSave, null, 2));
this.config = configToSave;
console.log('┌─ Configuration Saved ─────────────────────┐');
console.log(`│ Location: ${this.CONFIG_PATH}`);
console.log('└──────────────────────────────────────────┘');
logger.logBox('Configuration Saved', [`Location: ${this.CONFIG_PATH}`], 45, 'success');
} catch (error) {
console.error('Error saving configuration:', error);
logger.error(`Error saving configuration: ${error}`);
}
}
@@ -219,10 +217,7 @@ export class NupstDaemon {
* Helper method to log configuration errors consistently
*/
private logConfigError(message: string): void {
console.error('┌─ Configuration Error ─────────────────────┐');
console.error(`${message}`);
console.error("│ Please run 'nupst setup' first to create a configuration.");
console.error('└───────────────────────────────────────────┘');
logger.logBox('Configuration Error', [message, "Please run 'nupst setup' first to create a configuration."], 45, 'error');
}
/**

View File

@@ -34,12 +34,14 @@ export class MigrationRunner {
let currentConfig = config;
let anyMigrationsRan = false;
logger.dim('Checking for required config migrations...');
for (const migration of this.migrations) {
const shouldRun = await migration.shouldRun(currentConfig);
if (shouldRun) {
// Only show "checking" message when we actually need to migrate
if (!anyMigrationsRan) {
logger.dim('Checking for required config migrations...');
}
logger.info(`Running ${migration.getName()}...`);
currentConfig = await migration.migrate(currentConfig);
anyMigrationsRan = true;
@@ -49,7 +51,7 @@ export class MigrationRunner {
if (anyMigrationsRan) {
logger.success('Configuration migrations complete');
} else {
logger.dim('No migrations needed');
logger.success('config format ok');
}
return {

View File

@@ -525,6 +525,7 @@ export class NupstSnmp {
/**
* Determine power status based on UPS model and raw value
* Uses the value mappings defined in the OID sets
* @param upsModel UPS model
* @param powerStatusValue Raw power status value
* @returns Standardized power status
@@ -533,39 +534,28 @@ export class NupstSnmp {
upsModel: TUpsModel | undefined,
powerStatusValue: number,
): 'online' | 'onBattery' | 'unknown' {
if (upsModel === 'cyberpower') {
// CyberPower RMCARD205: upsBaseOutputStatus values
// 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc.
if (powerStatusValue === 2) {
return 'online';
} else if (powerStatusValue === 3) {
return 'onBattery';
}
} else if (upsModel === 'eaton') {
// Eaton UPS: xupsOutputSource values
// 3=normal/mains, 5=battery, etc.
if (powerStatusValue === 3) {
return 'online';
} else if (powerStatusValue === 5) {
return 'onBattery';
}
} else if (upsModel === 'apc') {
// APC UPS: upsBasicOutputStatus values
// 2=online, 3=onBattery, etc.
if (powerStatusValue === 2) {
return 'online';
} else if (powerStatusValue === 3) {
return 'onBattery';
}
} else {
// Default interpretation for other UPS models
if (powerStatusValue === 1) {
return 'online';
} else if (powerStatusValue === 2) {
return 'onBattery';
// 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';
}
}
}
// 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';
}
return 'unknown';
}

View File

@@ -11,37 +11,57 @@ export class UpsOidSets {
private static readonly UPS_OID_SETS: Record<TUpsModel, IOidSet> = {
// Cyberpower OIDs for RMCARD205 (based on CyberPower_MIB_v2.11)
cyberpower: {
POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus (2=online, 3=on battery)
POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus
BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage)
BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks)
POWER_STATUS_VALUES: {
online: 2, // upsBaseOutputStatus: 2=onLine
onBattery: 3, // upsBaseOutputStatus: 3=onBattery
},
},
// APC OIDs
apc: {
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // Power status (1=online, 2=on battery)
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // upsBasicOutputStatus
BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes
POWER_STATUS_VALUES: {
online: 2, // upsBasicOutputStatus: 2=onLine
onBattery: 3, // upsBasicOutputStatus: 3=onBattery
},
},
// Eaton OIDs
eaton: {
POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource (3=normal/mains, 5=battery)
POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage)
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds)
POWER_STATUS_VALUES: {
online: 3, // xupsOutputSource: 3=normal (mains power)
onBattery: 5, // xupsOutputSource: 5=battery
},
},
// TrippLite OIDs
tripplite: {
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // Power status
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // tlUpsOutputSource
BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes
POWER_STATUS_VALUES: {
online: 2, // tlUpsOutputSource: 2=normal (mains power)
onBattery: 3, // tlUpsOutputSource: 3=onBattery
},
},
// Liebert/Vertiv OIDs
liebert: {
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // Power status
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // lgpPwrOutputSource
BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes
POWER_STATUS_VALUES: {
online: 2, // lgpPwrOutputSource: 2=normal (mains power)
onBattery: 3, // lgpPwrOutputSource: 3=onBattery
},
},
// Custom OIDs (to be provided by the user)

View File

@@ -28,6 +28,13 @@ export interface IOidSet {
BATTERY_CAPACITY: string;
/** OID for battery runtime */
BATTERY_RUNTIME: string;
/** Power status value mappings */
POWER_STATUS_VALUES?: {
/** SNMP value that indicates UPS is online (on AC power) */
online: number;
/** SNMP value that indicates UPS is on battery */
onBattery: number;
};
}
/**

View File

@@ -50,11 +50,11 @@ WantedBy=multi-user.target
try {
await fs.access(configPath);
} catch (error) {
const boxWidth = 50;
logger.logBoxTitle('Configuration Error', boxWidth);
logger.logBoxLine(`No configuration file found at ${configPath}`);
logger.logBoxLine("Please run 'nupst add' first to create a UPS configuration.");
logger.logBoxEnd();
logger.log('');
logger.error('No configuration found');
logger.log(` ${theme.dim('Config file:')} ${configPath}`);
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to create a configuration')}`);
logger.log('');
throw new Error('Configuration not found');
}
}
@@ -138,16 +138,12 @@ WantedBy=multi-user.target
try {
// Enable debug mode if requested
if (debugMode) {
const boxWidth = 45;
logger.logBoxTitle('Debug Mode', boxWidth);
logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown');
logger.logBoxEnd();
console.log('');
logger.info('Debug Mode: SNMP debugging enabled');
console.log('');
this.daemon.getNupstSnmp().enableDebug();
}
// Display version information
this.daemon.getNupstSnmp().getNupst().logVersionInfo();
// Check if config exists first
try {
await this.checkConfigExists();
@@ -196,11 +192,11 @@ WantedBy=multi-user.target
}
// Display beautiful status
console.log('');
logger.log('');
if (isActive) {
console.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
logger.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
} else {
console.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`);
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`);
}
if (pid || memory || cpu) {
@@ -208,14 +204,14 @@ WantedBy=multi-user.target
if (pid) details.push(`PID: ${theme.dim(pid)}`);
if (memory) details.push(`Memory: ${theme.dim(memory)}`);
if (cpu) details.push(`CPU: ${theme.dim(cpu)}`);
console.log(` ${details.join(' ')}`);
logger.log(` ${details.join(' ')}`);
}
console.log('');
logger.log('');
} catch (error) {
console.log('');
console.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
console.log('');
logger.log('');
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
logger.log('');
}
}
@@ -232,7 +228,7 @@ WantedBy=multi-user.target
// Check if we have the new multi-UPS config format
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
console.log(theme.info(`UPS Devices (${config.upsDevices.length}):`));
logger.info(`UPS Devices (${config.upsDevices.length}):`);
// Show status for each UPS
for (const ups of config.upsDevices) {
@@ -240,7 +236,7 @@ WantedBy=multi-user.target
}
} else if (config.snmp) {
// Legacy single UPS configuration
console.log(theme.info('UPS Devices (1):'));
logger.info('UPS Devices (1):');
const legacyUps = {
id: 'default',
name: 'Default UPS',
@@ -251,16 +247,16 @@ WantedBy=multi-user.target
await this.displaySingleUpsStatus(legacyUps, snmp);
} else {
console.log('');
console.log(`${symbols.warning} ${theme.warning('No UPS devices configured')}`);
console.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
console.log('');
logger.log('');
logger.warn('No UPS devices configured');
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
logger.log('');
}
} catch (error) {
console.log('');
console.log(`${symbols.error} ${theme.error('Failed to retrieve UPS status')}`);
console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
console.log('');
logger.log('');
logger.error('Failed to retrieve UPS status');
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
logger.log('');
}
}
@@ -288,15 +284,15 @@ WantedBy=multi-user.target
}
// Display UPS name and power status
console.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
logger.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
// Display battery with color coding
const batteryColor = getBatteryColor(status.batteryCapacity);
const batterySymbol = status.batteryCapacity >= ups.thresholds.battery ? symbols.success : symbols.warning;
console.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
// Display host info
console.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
// Display groups if any
if (ups.groups && ups.groups.length > 0) {
@@ -305,17 +301,17 @@ WantedBy=multi-user.target
const group = config.groups?.find((g: { id: string }) => g.id === groupId);
return group ? group.name : groupId;
});
console.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
logger.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
}
console.log('');
logger.log('');
} catch (error) {
// Display error for this UPS
console.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
console.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
console.log('');
logger.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
logger.log('');
}
}