Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
263d69aef1 | |||
b6b7b43161 | |||
316c66c344 | |||
4debda856b | |||
0e7bcab499 | |||
7bf65d8495 | |||
f2ce0180d3 | |||
8c1be6555f | |||
1a5558e91f | |||
611a9ddd19 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/nupst",
|
"name": "@serve.zone/nupst",
|
||||||
"version": "4.2.0",
|
"version": "4.2.5",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"dev": "deno run --allow-all mod.ts",
|
"dev": "deno run --allow-all mod.ts",
|
||||||
|
@@ -123,9 +123,6 @@ export class UpsHandler {
|
|||||||
// Gather SNMP settings
|
// Gather SNMP settings
|
||||||
await this.gatherSnmpSettings(newUps.snmp, prompt);
|
await this.gatherSnmpSettings(newUps.snmp, prompt);
|
||||||
|
|
||||||
// Gather threshold settings
|
|
||||||
await this.gatherThresholdSettings(newUps.thresholds, prompt);
|
|
||||||
|
|
||||||
// Gather UPS model settings
|
// Gather UPS model settings
|
||||||
await this.gatherUpsModelSettings(newUps.snmp, prompt);
|
await this.gatherUpsModelSettings(newUps.snmp, prompt);
|
||||||
|
|
||||||
@@ -820,39 +817,6 @@ export class UpsHandler {
|
|||||||
snmpConfig.privKey = privKey.trim() || defaultPrivKey;
|
snmpConfig.privKey = privKey.trim() || defaultPrivKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gather threshold settings
|
|
||||||
* @param thresholds Thresholds configuration object to update
|
|
||||||
* @param prompt Function to prompt for user input
|
|
||||||
*/
|
|
||||||
private async gatherThresholdSettings(
|
|
||||||
thresholds: any,
|
|
||||||
prompt: (question: string) => Promise<string>,
|
|
||||||
): Promise<void> {
|
|
||||||
logger.log('');
|
|
||||||
logger.info('Shutdown Thresholds:');
|
|
||||||
|
|
||||||
// Battery threshold
|
|
||||||
const defaultBatteryThreshold = thresholds.battery || 60;
|
|
||||||
const batteryThresholdInput = await prompt(
|
|
||||||
`Battery percentage threshold [${defaultBatteryThreshold}%]: `,
|
|
||||||
);
|
|
||||||
const batteryThreshold = parseInt(batteryThresholdInput, 10);
|
|
||||||
thresholds.battery = batteryThresholdInput.trim() && !isNaN(batteryThreshold)
|
|
||||||
? batteryThreshold
|
|
||||||
: defaultBatteryThreshold;
|
|
||||||
|
|
||||||
// Runtime threshold
|
|
||||||
const defaultRuntimeThreshold = thresholds.runtime || 20;
|
|
||||||
const runtimeThresholdInput = await prompt(
|
|
||||||
`Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: `,
|
|
||||||
);
|
|
||||||
const runtimeThreshold = parseInt(runtimeThresholdInput, 10);
|
|
||||||
thresholds.runtime = runtimeThresholdInput.trim() && !isNaN(runtimeThreshold)
|
|
||||||
? runtimeThreshold
|
|
||||||
: defaultRuntimeThreshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gather UPS model settings
|
* Gather UPS model settings
|
||||||
* @param snmpConfig SNMP configuration object to update
|
* @param snmpConfig SNMP configuration object to update
|
||||||
|
14
ts/daemon.ts
14
ts/daemon.ts
@@ -4,7 +4,7 @@ import * as path from 'node:path';
|
|||||||
import { exec, execFile } from 'node:child_process';
|
import { exec, execFile } from 'node:child_process';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import { NupstSnmp } from './snmp/manager.ts';
|
import { NupstSnmp } from './snmp/manager.ts';
|
||||||
import type { ISnmpConfig } from './snmp/types.ts';
|
import type { ISnmpConfig, IUpsStatus as ISnmpUpsStatus } from './snmp/types.ts';
|
||||||
import { logger, type ITableColumn } from './logger.ts';
|
import { logger, type ITableColumn } from './logger.ts';
|
||||||
import { MigrationRunner } from './migrations/index.ts';
|
import { MigrationRunner } from './migrations/index.ts';
|
||||||
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
||||||
@@ -76,7 +76,7 @@ export interface INupstConfig {
|
|||||||
/**
|
/**
|
||||||
* UPS status tracking interface
|
* UPS status tracking interface
|
||||||
*/
|
*/
|
||||||
interface IUpsStatus {
|
export interface IUpsStatus {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
powerStatus: 'online' | 'onBattery' | 'unknown';
|
powerStatus: 'online' | 'onBattery' | 'unknown';
|
||||||
@@ -96,7 +96,7 @@ export class NupstDaemon {
|
|||||||
|
|
||||||
/** Default configuration */
|
/** Default configuration */
|
||||||
private readonly DEFAULT_CONFIG: INupstConfig = {
|
private readonly DEFAULT_CONFIG: INupstConfig = {
|
||||||
version: '4.1',
|
version: '4.2',
|
||||||
upsDevices: [
|
upsDevices: [
|
||||||
{
|
{
|
||||||
id: 'default',
|
id: 'default',
|
||||||
@@ -171,11 +171,13 @@ export class NupstDaemon {
|
|||||||
const { config: migratedConfig, migrated } = await migrationRunner.run(parsedConfig);
|
const { config: migratedConfig, migrated } = await migrationRunner.run(parsedConfig);
|
||||||
|
|
||||||
// Save migrated config back to disk if any migrations ran
|
// Save migrated config back to disk if any migrations ran
|
||||||
|
// Cast to INupstConfig since migrations ensure the output is valid
|
||||||
|
const validConfig = migratedConfig as unknown as INupstConfig;
|
||||||
if (migrated) {
|
if (migrated) {
|
||||||
this.config = migratedConfig;
|
this.config = validConfig;
|
||||||
await this.saveConfig(this.config);
|
await this.saveConfig(this.config);
|
||||||
} else {
|
} else {
|
||||||
this.config = migratedConfig;
|
this.config = validConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.config;
|
return this.config;
|
||||||
@@ -760,7 +762,7 @@ export class NupstDaemon {
|
|||||||
|
|
||||||
const rows: Array<Record<string, string>> = [];
|
const rows: Array<Record<string, string>> = [];
|
||||||
let emergencyDetected = false;
|
let emergencyDetected = false;
|
||||||
let emergencyUps: any = null;
|
let emergencyUps: { ups: IUpsConfig; status: ISnmpUpsStatus } | null = null;
|
||||||
|
|
||||||
// Check all UPS devices
|
// Check all UPS devices
|
||||||
for (const ups of this.config.upsDevices) {
|
for (const ups of this.config.upsDevices) {
|
||||||
|
@@ -28,18 +28,18 @@ export abstract class BaseMigration {
|
|||||||
/**
|
/**
|
||||||
* Check if this migration should run on the given config
|
* Check if this migration should run on the given config
|
||||||
*
|
*
|
||||||
* @param config - Raw configuration object to check
|
* @param config - Raw configuration object to check (unknown schema for migrations)
|
||||||
* @returns True if migration should run, false otherwise
|
* @returns True if migration should run, false otherwise
|
||||||
*/
|
*/
|
||||||
abstract shouldRun(config: any): Promise<boolean>;
|
abstract shouldRun(config: Record<string, unknown>): Promise<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform the migration on the given config
|
* Perform the migration on the given config
|
||||||
*
|
*
|
||||||
* @param config - Raw configuration object to migrate
|
* @param config - Raw configuration object to migrate (unknown schema for migrations)
|
||||||
* @returns Migrated configuration object
|
* @returns Migrated configuration object
|
||||||
*/
|
*/
|
||||||
abstract migrate(config: any): Promise<any>;
|
abstract migrate(config: Record<string, unknown>): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get human-readable name for this migration
|
* Get human-readable name for this migration
|
||||||
|
@@ -19,7 +19,7 @@ export class MigrationRunner {
|
|||||||
new MigrationV1ToV2(),
|
new MigrationV1ToV2(),
|
||||||
new MigrationV3ToV4(),
|
new MigrationV3ToV4(),
|
||||||
new MigrationV4_0ToV4_1(),
|
new MigrationV4_0ToV4_1(),
|
||||||
// Add future migrations here (v4.2, v4.3, etc.)
|
// Add future migrations here (v4.3, v4.4, etc.)
|
||||||
];
|
];
|
||||||
|
|
||||||
// Sort by version order to ensure they run in sequence
|
// Sort by version order to ensure they run in sequence
|
||||||
@@ -32,7 +32,9 @@ export class MigrationRunner {
|
|||||||
* @param config - Raw configuration object to migrate
|
* @param config - Raw configuration object to migrate
|
||||||
* @returns Migrated configuration and whether migrations ran
|
* @returns Migrated configuration and whether migrations ran
|
||||||
*/
|
*/
|
||||||
async run(config: any): Promise<{ config: any; migrated: boolean }> {
|
async run(
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
): Promise<{ config: Record<string, unknown>; migrated: boolean }> {
|
||||||
let currentConfig = config;
|
let currentConfig = config;
|
||||||
let anyMigrationsRan = false;
|
let anyMigrationsRan = false;
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ import { logger } from '../logger.ts';
|
|||||||
* 3. Adds empty actions array to UPS devices without actions
|
* 3. Adds empty actions array to UPS devices without actions
|
||||||
* 4. Adds empty actions array to groups
|
* 4. Adds empty actions array to groups
|
||||||
*
|
*
|
||||||
* Transforms v4.0 format:
|
* Transforms v4.0 format (with UPS-level thresholds):
|
||||||
* {
|
* {
|
||||||
* version: "4.0",
|
* version: "4.0",
|
||||||
* upsDevices: [
|
* upsDevices: [
|
||||||
@@ -24,7 +24,7 @@ import { logger } from '../logger.ts';
|
|||||||
* ]
|
* ]
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* To v4.1 format:
|
* To v4.1 format (with action-level thresholds):
|
||||||
* {
|
* {
|
||||||
* version: "4.1",
|
* version: "4.1",
|
||||||
* upsDevices: [
|
* upsDevices: [
|
||||||
@@ -37,7 +37,7 @@ import { logger } from '../logger.ts';
|
|||||||
* {
|
* {
|
||||||
* type: "shutdown",
|
* type: "shutdown",
|
||||||
* thresholds: { battery: 60, runtime: 20 },
|
* thresholds: { battery: 60, runtime: 20 },
|
||||||
* onlyOnThresholdViolation: true,
|
* triggerMode: "onlyThresholds",
|
||||||
* shutdownDelay: 5
|
* shutdownDelay: 5
|
||||||
* }
|
* }
|
||||||
* ]
|
* ]
|
||||||
@@ -49,30 +49,31 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
|
|||||||
readonly fromVersion = '4.0';
|
readonly fromVersion = '4.0';
|
||||||
readonly toVersion = '4.1';
|
readonly toVersion = '4.1';
|
||||||
|
|
||||||
async shouldRun(config: any): Promise<boolean> {
|
async shouldRun(config: Record<string, unknown>): Promise<boolean> {
|
||||||
// Run if config is version 4.0 or missing version with v4 structure
|
// Run if config is version 4.0
|
||||||
if (config.version === '4.0') {
|
if (config.version === '4.0') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also run if config has upsDevices with thresholds at UPS level (v4.0 format)
|
// Also run if config has upsDevices with thresholds at UPS level (v4.0 format)
|
||||||
if (config.upsDevices && config.upsDevices.length > 0) {
|
if (Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
|
||||||
const firstDevice = config.upsDevices[0];
|
const firstDevice = config.upsDevices[0] as Record<string, unknown>;
|
||||||
// v4.0 has thresholds at UPS level, v4.1 has them in actions
|
// v4.0 has thresholds at UPS level, v4.1 has them in actions
|
||||||
return firstDevice.thresholds !== undefined && firstDevice.actions === undefined;
|
return firstDevice.thresholds !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async migrate(config: any): Promise<any> {
|
async migrate(config: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||||
logger.info(`${this.getName()}: Migrating v4.0 config to v4.1 format...`);
|
logger.info(`${this.getName()}: Migrating v4.0 config to v4.1 format...`);
|
||||||
logger.dim(` - Moving thresholds from UPS level to action level`);
|
logger.dim(` - Moving thresholds from UPS level to action level`);
|
||||||
logger.dim(` - Creating default shutdown actions from existing thresholds`);
|
logger.dim(` - Creating default shutdown actions from existing thresholds`);
|
||||||
|
|
||||||
// Migrate UPS devices
|
// Migrate UPS devices
|
||||||
const migratedDevices = (config.upsDevices || []).map((device: any) => {
|
const devices = (config.upsDevices as Array<Record<string, unknown>>) || [];
|
||||||
const migrated: any = {
|
const migratedDevices = devices.map((device) => {
|
||||||
|
const migrated: Record<string, unknown> = {
|
||||||
id: device.id,
|
id: device.id,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
snmp: device.snmp,
|
snmp: device.snmp,
|
||||||
@@ -80,20 +81,21 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// If device has thresholds at UPS level, convert to shutdown action
|
// If device has thresholds at UPS level, convert to shutdown action
|
||||||
if (device.thresholds) {
|
const deviceThresholds = device.thresholds as { battery: number; runtime: number } | undefined;
|
||||||
|
if (deviceThresholds) {
|
||||||
migrated.actions = [
|
migrated.actions = [
|
||||||
{
|
{
|
||||||
type: 'shutdown',
|
type: 'shutdown',
|
||||||
thresholds: {
|
thresholds: {
|
||||||
battery: device.thresholds.battery,
|
battery: deviceThresholds.battery,
|
||||||
runtime: device.thresholds.runtime,
|
runtime: deviceThresholds.runtime,
|
||||||
},
|
},
|
||||||
triggerMode: 'onlyThresholds', // Preserve old behavior (only on threshold violation)
|
triggerMode: 'onlyThresholds', // Preserve old behavior (only on threshold violation)
|
||||||
shutdownDelay: 5, // Default delay
|
shutdownDelay: 5, // Default delay
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
logger.dim(
|
logger.dim(
|
||||||
` → ${device.name}: Created shutdown action (battery: ${device.thresholds.battery}%, runtime: ${device.thresholds.runtime}min)`,
|
` → ${device.name}: Created shutdown action (battery: ${deviceThresholds.battery}%, runtime: ${deviceThresholds.runtime}min)`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// No thresholds, just add empty actions array
|
// No thresholds, just add empty actions array
|
||||||
@@ -104,7 +106,8 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add actions to groups
|
// Add actions to groups
|
||||||
const migratedGroups = (config.groups || []).map((group: any) => ({
|
const groups = (config.groups as Array<Record<string, unknown>>) || [];
|
||||||
|
const migratedGroups = groups.map((group) => ({
|
||||||
...group,
|
...group,
|
||||||
actions: group.actions || [],
|
actions: group.actions || [],
|
||||||
}));
|
}));
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { promises as fs } from 'node:fs';
|
import { promises as fs } from 'node:fs';
|
||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import { NupstDaemon } from './daemon.ts';
|
import { NupstDaemon, type IUpsConfig } from './daemon.ts';
|
||||||
|
import { NupstSnmp } from './snmp/manager.ts';
|
||||||
import { logger } from './logger.ts';
|
import { logger } from './logger.ts';
|
||||||
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
||||||
|
|
||||||
@@ -277,14 +278,23 @@ WantedBy=multi-user.target
|
|||||||
await this.displaySingleUpsStatus(ups, snmp);
|
await this.displaySingleUpsStatus(ups, snmp);
|
||||||
}
|
}
|
||||||
} else if (config.snmp) {
|
} else if (config.snmp) {
|
||||||
// Legacy single UPS configuration
|
// Legacy single UPS configuration (v1/v2 format)
|
||||||
logger.info('UPS Devices (1):');
|
logger.info('UPS Devices (1):');
|
||||||
const legacyUps = {
|
const legacyUps: IUpsConfig = {
|
||||||
id: 'default',
|
id: 'default',
|
||||||
name: 'Default UPS',
|
name: 'Default UPS',
|
||||||
snmp: config.snmp,
|
snmp: config.snmp,
|
||||||
thresholds: config.thresholds,
|
|
||||||
groups: [],
|
groups: [],
|
||||||
|
actions: config.thresholds
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: 'shutdown',
|
||||||
|
thresholds: config.thresholds,
|
||||||
|
triggerMode: 'onlyThresholds',
|
||||||
|
shutdownDelay: 5,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.displaySingleUpsStatus(legacyUps, snmp);
|
await this.displaySingleUpsStatus(legacyUps, snmp);
|
||||||
@@ -307,7 +317,7 @@ WantedBy=multi-user.target
|
|||||||
* @param ups UPS configuration
|
* @param ups UPS configuration
|
||||||
* @param snmp SNMP manager
|
* @param snmp SNMP manager
|
||||||
*/
|
*/
|
||||||
private async displaySingleUpsStatus(ups: any, snmp: any): Promise<void> {
|
private async displaySingleUpsStatus(ups: IUpsConfig, snmp: NupstSnmp): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Create a test config with a short timeout
|
// Create a test config with a short timeout
|
||||||
const testConfig = {
|
const testConfig = {
|
||||||
@@ -330,7 +340,16 @@ WantedBy=multi-user.target
|
|||||||
|
|
||||||
// Display battery with color coding
|
// Display battery with color coding
|
||||||
const batteryColor = getBatteryColor(status.batteryCapacity);
|
const batteryColor = getBatteryColor(status.batteryCapacity);
|
||||||
const batterySymbol = status.batteryCapacity >= ups.thresholds.battery ? symbols.success : symbols.warning;
|
|
||||||
|
// Get threshold from actions (if any action has thresholds defined)
|
||||||
|
const actionWithThresholds = ups.actions?.find((action) => action.thresholds);
|
||||||
|
const batteryThreshold = actionWithThresholds?.thresholds?.battery;
|
||||||
|
const batterySymbol = batteryThreshold !== undefined && status.batteryCapacity >= batteryThreshold
|
||||||
|
? symbols.success
|
||||||
|
: batteryThreshold !== undefined
|
||||||
|
? symbols.warning
|
||||||
|
: '';
|
||||||
|
|
||||||
logger.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
|
// Display host info
|
||||||
|
Reference in New Issue
Block a user