import * as plugins from '../plugins.js'; import { Device } from '../abstract/device.abstract.js'; import { NutProtocol, NUT_COMMANDS, NUT_VARIABLES, type TNutStatusFlag } from './ups.classes.nutprotocol.js'; import { UpsSnmpHandler, type TUpsBatteryStatus, type TUpsOutputSource } from './ups.classes.upssnmp.js'; import type { IDeviceInfo, IRetryOptions } from '../interfaces/index.js'; /** * UPS status enumeration */ export type TUpsStatus = 'online' | 'onbattery' | 'lowbattery' | 'charging' | 'discharging' | 'bypass' | 'offline' | 'error' | 'unknown'; /** * UPS protocol type */ export type TUpsProtocol = 'nut' | 'snmp'; /** * UPS device information */ export interface IUpsDeviceInfo extends IDeviceInfo { type: 'ups'; protocol: TUpsProtocol; upsName?: string; // NUT ups name manufacturer: string; model: string; serialNumber?: string; firmwareVersion?: string; } /** * UPS battery information */ export interface IUpsBatteryInfo { charge: number; // 0-100% runtime: number; // seconds remaining voltage: number; // volts temperature?: number; // celsius status: 'normal' | 'low' | 'depleted' | 'unknown'; } /** * UPS input/output power info */ export interface IUpsPowerInfo { inputVoltage: number; inputFrequency?: number; outputVoltage: number; outputFrequency?: number; outputCurrent?: number; outputPower?: number; load: number; // 0-100% } /** * Full UPS status */ export interface IUpsFullStatus { status: TUpsStatus; battery: IUpsBatteryInfo; power: IUpsPowerInfo; alarms: string[]; secondsOnBattery: number; } /** * UPS Device class supporting both NUT and SNMP protocols */ export class UpsDevice extends Device { private nutProtocol: NutProtocol | null = null; private snmpHandler: UpsSnmpHandler | null = null; private upsProtocol: TUpsProtocol; private upsName: string; private snmpCommunity: string; private _upsStatus: TUpsStatus = 'unknown'; private _manufacturer: string = ''; private _model: string = ''; private _batteryCharge: number = 0; private _batteryRuntime: number = 0; private _inputVoltage: number = 0; private _outputVoltage: number = 0; private _load: number = 0; constructor( info: IDeviceInfo, options: { protocol: TUpsProtocol; upsName?: string; // Required for NUT snmpCommunity?: string; // For SNMP }, retryOptions?: IRetryOptions ) { super(info, retryOptions); this.upsProtocol = options.protocol; this.upsName = options.upsName || 'ups'; this.snmpCommunity = options.snmpCommunity || 'public'; } // Getters for UPS properties public get upsStatus(): TUpsStatus { return this._upsStatus; } public get upsManufacturer(): string { return this._manufacturer; } public get upsModel(): string { return this._model; } public get batteryCharge(): number { return this._batteryCharge; } public get batteryRuntime(): number { return this._batteryRuntime; } public get inputVoltage(): number { return this._inputVoltage; } public get outputVoltage(): number { return this._outputVoltage; } public get load(): number { return this._load; } public get protocol(): TUpsProtocol { return this.upsProtocol; } /** * Connect to UPS */ protected async doConnect(): Promise { if (this.upsProtocol === 'nut') { await this.connectNut(); } else { await this.connectSnmp(); } } /** * Connect via NUT protocol */ private async connectNut(): Promise { this.nutProtocol = new NutProtocol(this.address, this.port); await this.nutProtocol.connect(); // Get device info const deviceInfo = await this.nutProtocol.getDeviceInfo(this.upsName); this._manufacturer = deviceInfo.manufacturer; this._model = deviceInfo.model; this.manufacturer = deviceInfo.manufacturer; this.model = deviceInfo.model; this.serialNumber = deviceInfo.serial; // Get initial status await this.refreshStatus(); } /** * Connect via SNMP protocol */ private async connectSnmp(): Promise { this.snmpHandler = new UpsSnmpHandler(this.address, { community: this.snmpCommunity, port: this.port, }); // Verify it's a UPS const isUps = await this.snmpHandler.isUpsDevice(); if (!isUps) { this.snmpHandler.close(); this.snmpHandler = null; throw new Error('Device does not support UPS-MIB'); } // Get identity const identity = await this.snmpHandler.getIdentity(); this._manufacturer = identity.manufacturer; this._model = identity.model; this.manufacturer = identity.manufacturer; this.model = identity.model; this.firmwareVersion = identity.softwareVersion; // Get initial status await this.refreshStatus(); } /** * Disconnect from UPS */ protected async doDisconnect(): Promise { if (this.nutProtocol) { await this.nutProtocol.disconnect(); this.nutProtocol = null; } if (this.snmpHandler) { this.snmpHandler.close(); this.snmpHandler = null; } } /** * Refresh UPS status */ public async refreshStatus(): Promise { if (this.upsProtocol === 'nut' && this.nutProtocol) { await this.refreshNutStatus(); } else if (this.snmpHandler) { await this.refreshSnmpStatus(); } else { throw new Error('Not connected'); } this.emit('status:updated', this.getDeviceInfo()); } /** * Refresh status via NUT */ private async refreshNutStatus(): Promise { if (!this.nutProtocol) return; const status = await this.nutProtocol.getUpsStatus(this.upsName); this._batteryCharge = status.batteryCharge; this._batteryRuntime = status.batteryRuntime; this._inputVoltage = status.inputVoltage; this._outputVoltage = status.outputVoltage; this._load = status.load; // Convert NUT status flags to our status this._upsStatus = this.nutStatusToUpsStatus(status.status); } /** * Refresh status via SNMP */ private async refreshSnmpStatus(): Promise { if (!this.snmpHandler) return; const status = await this.snmpHandler.getFullStatus(); this._batteryCharge = status.estimatedChargeRemaining; this._batteryRuntime = status.estimatedMinutesRemaining * 60; // Convert to seconds this._inputVoltage = status.inputVoltage; this._outputVoltage = status.outputVoltage; this._load = status.outputPercentLoad; // Convert SNMP status to our status this._upsStatus = this.snmpStatusToUpsStatus(status.outputSource, status.batteryStatus); } /** * Convert NUT status flags to TUpsStatus */ private nutStatusToUpsStatus(flags: TNutStatusFlag[]): TUpsStatus { if (flags.includes('OFF')) return 'offline'; if (flags.includes('LB')) return 'lowbattery'; if (flags.includes('OB')) return 'onbattery'; if (flags.includes('BYPASS')) return 'bypass'; if (flags.includes('CHRG')) return 'charging'; if (flags.includes('DISCHRG')) return 'discharging'; if (flags.includes('OL')) return 'online'; return 'unknown'; } /** * Convert SNMP status to TUpsStatus */ private snmpStatusToUpsStatus(source: TUpsOutputSource, battery: TUpsBatteryStatus): TUpsStatus { if (source === 'none') return 'offline'; if (source === 'battery') { if (battery === 'batteryLow') return 'lowbattery'; if (battery === 'batteryDepleted') return 'lowbattery'; return 'onbattery'; } if (source === 'bypass') return 'bypass'; if (source === 'normal') return 'online'; if (source === 'booster' || source === 'reducer') return 'online'; return 'unknown'; } /** * Get battery information */ public async getBatteryInfo(): Promise { if (this.upsProtocol === 'nut' && this.nutProtocol) { const vars = await this.nutProtocol.getVariables(this.upsName, [ NUT_VARIABLES.batteryCharge, NUT_VARIABLES.batteryRuntime, NUT_VARIABLES.batteryVoltage, NUT_VARIABLES.batteryTemperature, ]); return { charge: parseFloat(vars.get(NUT_VARIABLES.batteryCharge) || '0'), runtime: parseFloat(vars.get(NUT_VARIABLES.batteryRuntime) || '0'), voltage: parseFloat(vars.get(NUT_VARIABLES.batteryVoltage) || '0'), temperature: vars.has(NUT_VARIABLES.batteryTemperature) ? parseFloat(vars.get(NUT_VARIABLES.batteryTemperature)!) : undefined, status: 'normal', }; } else if (this.snmpHandler) { const battery = await this.snmpHandler.getBatteryStatus(); const statusMap: Record = { unknown: 'unknown', batteryNormal: 'normal', batteryLow: 'low', batteryDepleted: 'depleted', }; return { charge: battery.estimatedChargeRemaining, runtime: battery.estimatedMinutesRemaining * 60, voltage: battery.voltage, temperature: battery.temperature || undefined, status: statusMap[battery.status], }; } throw new Error('Not connected'); } /** * Get power information */ public async getPowerInfo(): Promise { if (this.upsProtocol === 'nut' && this.nutProtocol) { const vars = await this.nutProtocol.getVariables(this.upsName, [ NUT_VARIABLES.inputVoltage, NUT_VARIABLES.inputFrequency, NUT_VARIABLES.outputVoltage, NUT_VARIABLES.outputCurrent, NUT_VARIABLES.upsLoad, ]); return { inputVoltage: parseFloat(vars.get(NUT_VARIABLES.inputVoltage) || '0'), inputFrequency: vars.has(NUT_VARIABLES.inputFrequency) ? parseFloat(vars.get(NUT_VARIABLES.inputFrequency)!) : undefined, outputVoltage: parseFloat(vars.get(NUT_VARIABLES.outputVoltage) || '0'), outputCurrent: vars.has(NUT_VARIABLES.outputCurrent) ? parseFloat(vars.get(NUT_VARIABLES.outputCurrent)!) : undefined, load: parseFloat(vars.get(NUT_VARIABLES.upsLoad) || '0'), }; } else if (this.snmpHandler) { const [input, output] = await Promise.all([ this.snmpHandler.getInputStatus(), this.snmpHandler.getOutputStatus(), ]); return { inputVoltage: input.voltage, inputFrequency: input.frequency, outputVoltage: output.voltage, outputFrequency: output.frequency, outputCurrent: output.current, outputPower: output.power, load: output.percentLoad, }; } throw new Error('Not connected'); } /** * Get full status */ public async getFullStatus(): Promise { const [battery, power] = await Promise.all([ this.getBatteryInfo(), this.getPowerInfo(), ]); let secondsOnBattery = 0; const alarms: string[] = []; if (this.upsProtocol === 'nut' && this.nutProtocol) { const vars = await this.nutProtocol.getVariables(this.upsName, [ NUT_VARIABLES.upsStatus, NUT_VARIABLES.upsAlarm, ]); const alarm = vars.get(NUT_VARIABLES.upsAlarm); if (alarm) { alarms.push(alarm); } } else if (this.snmpHandler) { const snmpStatus = await this.snmpHandler.getFullStatus(); secondsOnBattery = snmpStatus.secondsOnBattery; if (snmpStatus.alarmsPresent > 0) { alarms.push(`${snmpStatus.alarmsPresent} alarm(s) present`); } } return { status: this._upsStatus, battery, power, alarms, secondsOnBattery, }; } /** * Run a UPS command (NUT only) */ public async runCommand(command: string): Promise { if (this.upsProtocol !== 'nut' || !this.nutProtocol) { throw new Error('Commands only supported via NUT protocol'); } const result = await this.nutProtocol.runCommand(this.upsName, command); this.emit('command:executed', { command, success: result }); return result; } /** * Start battery test */ public async startBatteryTest(type: 'quick' | 'deep' = 'quick'): Promise { const command = type === 'deep' ? NUT_COMMANDS.testBatteryStartDeep : NUT_COMMANDS.testBatteryStartQuick; return this.runCommand(command); } /** * Stop battery test */ public async stopBatteryTest(): Promise { return this.runCommand(NUT_COMMANDS.testBatteryStop); } /** * Toggle beeper */ public async toggleBeeper(): Promise { return this.runCommand(NUT_COMMANDS.beeperToggle); } /** * Get device info */ public getDeviceInfo(): IUpsDeviceInfo { return { id: this.id, name: this.name, type: 'ups', address: this.address, port: this.port, status: this.status, protocol: this.upsProtocol, upsName: this.upsName, manufacturer: this._manufacturer, model: this._model, serialNumber: this.serialNumber, firmwareVersion: this.firmwareVersion, }; } /** * Create UPS device from discovery */ public static fromDiscovery( data: { id: string; name: string; address: string; port?: number; protocol: TUpsProtocol; upsName?: string; community?: string; }, retryOptions?: IRetryOptions ): UpsDevice { const info: IDeviceInfo = { id: data.id, name: data.name, type: 'ups', address: data.address, port: data.port ?? (data.protocol === 'nut' ? 3493 : 161), status: 'unknown', }; return new UpsDevice( info, { protocol: data.protocol, upsName: data.upsName, snmpCommunity: data.community, }, retryOptions ); } /** * Probe for UPS device (NUT or SNMP) */ public static async probe( address: string, options?: { nutPort?: number; snmpPort?: number; snmpCommunity?: string; timeout?: number; } ): Promise<{ protocol: TUpsProtocol; port: number } | null> { const nutPort = options?.nutPort ?? 3493; const snmpPort = options?.snmpPort ?? 161; const community = options?.snmpCommunity ?? 'public'; // Try NUT first const nutAvailable = await NutProtocol.probe(address, nutPort, options?.timeout); if (nutAvailable) { return { protocol: 'nut', port: nutPort }; } // Try SNMP UPS-MIB try { const handler = new UpsSnmpHandler(address, { community, port: snmpPort, timeout: options?.timeout ?? 3000 }); const isUps = await handler.isUpsDevice(); handler.close(); if (isUps) { return { protocol: 'snmp', port: snmpPort }; } } catch { // Ignore SNMP errors } return null; } } // Re-export types export { NUT_COMMANDS, NUT_VARIABLES, type TNutStatusFlag } from './ups.classes.nutprotocol.js'; export { UPS_SNMP_OIDS, type TUpsBatteryStatus, type TUpsOutputSource } from './ups.classes.upssnmp.js';