feat(devicemanager): Introduce a UniversalDevice architecture with composable Feature system; add extensive new device/protocol support and discovery/refactors
This commit is contained in:
@@ -1,471 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
/**
|
||||
* NUT Protocol variable definitions
|
||||
*/
|
||||
export const NUT_VARIABLES = {
|
||||
// Device info
|
||||
deviceMfr: 'device.mfr',
|
||||
deviceModel: 'device.model',
|
||||
deviceSerial: 'device.serial',
|
||||
deviceType: 'device.type',
|
||||
|
||||
// UPS status
|
||||
upsStatus: 'ups.status',
|
||||
upsAlarm: 'ups.alarm',
|
||||
upsTime: 'ups.time',
|
||||
upsLoad: 'ups.load',
|
||||
upsTemperature: 'ups.temperature',
|
||||
|
||||
// Battery
|
||||
batteryCharge: 'battery.charge',
|
||||
batteryRuntime: 'battery.runtime',
|
||||
batteryVoltage: 'battery.voltage',
|
||||
batteryVoltageNominal: 'battery.voltage.nominal',
|
||||
batteryType: 'battery.type',
|
||||
batteryDate: 'battery.date',
|
||||
batteryTemperature: 'battery.temperature',
|
||||
|
||||
// Input
|
||||
inputVoltage: 'input.voltage',
|
||||
inputVoltageNominal: 'input.voltage.nominal',
|
||||
inputFrequency: 'input.frequency',
|
||||
inputFrequencyNominal: 'input.frequency.nominal',
|
||||
inputTransferHigh: 'input.transfer.high',
|
||||
inputTransferLow: 'input.transfer.low',
|
||||
|
||||
// Output
|
||||
outputVoltage: 'output.voltage',
|
||||
outputVoltageNominal: 'output.voltage.nominal',
|
||||
outputFrequency: 'output.frequency',
|
||||
outputCurrent: 'output.current',
|
||||
};
|
||||
|
||||
/**
|
||||
* NUT instant commands
|
||||
*/
|
||||
export const NUT_COMMANDS = {
|
||||
testBatteryStart: 'test.battery.start',
|
||||
testBatteryStartQuick: 'test.battery.start.quick',
|
||||
testBatteryStartDeep: 'test.battery.start.deep',
|
||||
testBatteryStop: 'test.battery.stop',
|
||||
calibrateStart: 'calibrate.start',
|
||||
calibrateStop: 'calibrate.stop',
|
||||
shutdown: 'shutdown.return',
|
||||
shutdownStayOff: 'shutdown.stayoff',
|
||||
shutdownStop: 'shutdown.stop',
|
||||
shutdownReboot: 'shutdown.reboot',
|
||||
beeperEnable: 'beeper.enable',
|
||||
beeperDisable: 'beeper.disable',
|
||||
beeperMute: 'beeper.mute',
|
||||
beeperToggle: 'beeper.toggle',
|
||||
loadOff: 'load.off',
|
||||
loadOn: 'load.on',
|
||||
};
|
||||
|
||||
/**
|
||||
* UPS status flags from NUT
|
||||
*/
|
||||
export type TNutStatusFlag =
|
||||
| 'OL' // Online (on utility power)
|
||||
| 'OB' // On battery
|
||||
| 'LB' // Low battery
|
||||
| 'HB' // High battery
|
||||
| 'RB' // Replace battery
|
||||
| 'CHRG' // Charging
|
||||
| 'DISCHRG' // Discharging
|
||||
| 'BYPASS' // On bypass
|
||||
| 'CAL' // Calibrating
|
||||
| 'OFF' // Offline
|
||||
| 'OVER' // Overloaded
|
||||
| 'TRIM' // Trimming voltage
|
||||
| 'BOOST' // Boosting voltage
|
||||
| 'FSD'; // Forced shutdown
|
||||
|
||||
/**
|
||||
* NUT UPS information
|
||||
*/
|
||||
export interface INutUpsInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* NUT variable
|
||||
*/
|
||||
export interface INutVariable {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* NUT Protocol handler for Network UPS Tools
|
||||
* TCP-based text protocol on port 3493
|
||||
*/
|
||||
export class NutProtocol {
|
||||
private socket: plugins.net.Socket | null = null;
|
||||
private address: string;
|
||||
private port: number;
|
||||
private connected: boolean = false;
|
||||
private responseBuffer: string = '';
|
||||
private responseResolver: ((value: string[]) => void) | null = null;
|
||||
private responseRejecter: ((error: Error) => void) | null = null;
|
||||
|
||||
constructor(address: string, port: number = 3493) {
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to NUT server
|
||||
*/
|
||||
public async connect(): Promise<void> {
|
||||
if (this.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.socket = new plugins.net.Socket();
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
if (this.socket) {
|
||||
this.socket.destroy();
|
||||
this.socket = null;
|
||||
}
|
||||
reject(new Error(`Connection timeout to ${this.address}:${this.port}`));
|
||||
}, 5000);
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
clearTimeout(timeout);
|
||||
this.connected = true;
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.socket.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
this.connected = false;
|
||||
if (this.responseRejecter) {
|
||||
this.responseRejecter(err);
|
||||
this.responseRejecter = null;
|
||||
this.responseResolver = null;
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.socket.on('data', (data) => {
|
||||
this.handleData(data);
|
||||
});
|
||||
|
||||
this.socket.on('close', () => {
|
||||
this.connected = false;
|
||||
if (this.responseRejecter) {
|
||||
this.responseRejecter(new Error('Connection closed'));
|
||||
this.responseRejecter = null;
|
||||
this.responseResolver = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.connect(this.port, this.address);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from NUT server
|
||||
*/
|
||||
public async disconnect(): Promise<void> {
|
||||
if (!this.connected || !this.socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.sendCommand('LOGOUT');
|
||||
} catch {
|
||||
// Ignore logout errors
|
||||
}
|
||||
|
||||
this.socket.destroy();
|
||||
this.socket = null;
|
||||
this.connected = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connected
|
||||
*/
|
||||
public get isConnected(): boolean {
|
||||
return this.connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming data
|
||||
*/
|
||||
private handleData(data: Buffer): void {
|
||||
this.responseBuffer += data.toString();
|
||||
|
||||
// Check for complete response (ends with newline)
|
||||
const lines = this.responseBuffer.split('\n');
|
||||
|
||||
// Check if we have a complete response
|
||||
if (this.responseBuffer.endsWith('\n')) {
|
||||
const responseLines = lines.filter((l) => l.trim().length > 0);
|
||||
this.responseBuffer = '';
|
||||
|
||||
if (this.responseResolver) {
|
||||
this.responseResolver(responseLines);
|
||||
this.responseResolver = null;
|
||||
this.responseRejecter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send command and get response
|
||||
*/
|
||||
private async sendCommand(command: string): Promise<string[]> {
|
||||
if (!this.socket || !this.connected) {
|
||||
throw new Error('Not connected to NUT server');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.responseResolver = resolve;
|
||||
this.responseRejecter = reject;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
this.responseResolver = null;
|
||||
this.responseRejecter = null;
|
||||
reject(new Error(`Command timeout: ${command}`));
|
||||
}, 10000);
|
||||
|
||||
this.responseResolver = (lines) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(lines);
|
||||
};
|
||||
|
||||
this.responseRejecter = (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
};
|
||||
|
||||
this.socket!.write(`${command}\n`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List available UPS devices
|
||||
*/
|
||||
public async listUps(): Promise<INutUpsInfo[]> {
|
||||
await this.ensureConnected();
|
||||
const response = await this.sendCommand('LIST UPS');
|
||||
|
||||
const upsList: INutUpsInfo[] = [];
|
||||
|
||||
for (const line of response) {
|
||||
// Format: UPS <name> "<description>"
|
||||
const match = line.match(/^UPS\s+(\S+)\s+"([^"]*)"/);
|
||||
if (match) {
|
||||
upsList.push({
|
||||
name: match[1],
|
||||
description: match[2],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return upsList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all variables for a UPS
|
||||
*/
|
||||
public async listVariables(upsName: string): Promise<INutVariable[]> {
|
||||
await this.ensureConnected();
|
||||
const response = await this.sendCommand(`LIST VAR ${upsName}`);
|
||||
|
||||
const variables: INutVariable[] = [];
|
||||
|
||||
for (const line of response) {
|
||||
// Format: VAR <ups> <name> "<value>"
|
||||
const match = line.match(/^VAR\s+\S+\s+(\S+)\s+"([^"]*)"/);
|
||||
if (match) {
|
||||
variables.push({
|
||||
name: match[1],
|
||||
value: match[2],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific variable value
|
||||
*/
|
||||
public async getVariable(upsName: string, varName: string): Promise<string | null> {
|
||||
await this.ensureConnected();
|
||||
const response = await this.sendCommand(`GET VAR ${upsName} ${varName}`);
|
||||
|
||||
for (const line of response) {
|
||||
// Format: VAR <ups> <name> "<value>"
|
||||
const match = line.match(/^VAR\s+\S+\s+\S+\s+"([^"]*)"/);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
// Handle error responses
|
||||
if (line.startsWith('ERR')) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple variables at once
|
||||
*/
|
||||
public async getVariables(upsName: string, varNames: string[]): Promise<Map<string, string>> {
|
||||
const results = new Map<string, string>();
|
||||
|
||||
for (const varName of varNames) {
|
||||
const value = await this.getVariable(upsName, varName);
|
||||
if (value !== null) {
|
||||
results.set(varName, value);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an instant command
|
||||
*/
|
||||
public async runCommand(upsName: string, command: string): Promise<boolean> {
|
||||
await this.ensureConnected();
|
||||
const response = await this.sendCommand(`INSTCMD ${upsName} ${command}`);
|
||||
|
||||
for (const line of response) {
|
||||
if (line === 'OK') {
|
||||
return true;
|
||||
}
|
||||
if (line.startsWith('ERR')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* List available commands for a UPS
|
||||
*/
|
||||
public async listCommands(upsName: string): Promise<string[]> {
|
||||
await this.ensureConnected();
|
||||
const response = await this.sendCommand(`LIST CMD ${upsName}`);
|
||||
|
||||
const commands: string[] = [];
|
||||
|
||||
for (const line of response) {
|
||||
// Format: CMD <ups> <command>
|
||||
const match = line.match(/^CMD\s+\S+\s+(\S+)/);
|
||||
if (match) {
|
||||
commands.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse UPS status string into flags
|
||||
*/
|
||||
public parseStatus(statusString: string): TNutStatusFlag[] {
|
||||
return statusString.split(/\s+/).filter((s) => s.length > 0) as TNutStatusFlag[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive UPS status
|
||||
*/
|
||||
public async getUpsStatus(upsName: string): Promise<{
|
||||
status: TNutStatusFlag[];
|
||||
batteryCharge: number;
|
||||
batteryRuntime: number;
|
||||
inputVoltage: number;
|
||||
outputVoltage: number;
|
||||
load: number;
|
||||
}> {
|
||||
const vars = await this.getVariables(upsName, [
|
||||
NUT_VARIABLES.upsStatus,
|
||||
NUT_VARIABLES.batteryCharge,
|
||||
NUT_VARIABLES.batteryRuntime,
|
||||
NUT_VARIABLES.inputVoltage,
|
||||
NUT_VARIABLES.outputVoltage,
|
||||
NUT_VARIABLES.upsLoad,
|
||||
]);
|
||||
|
||||
return {
|
||||
status: this.parseStatus(vars.get(NUT_VARIABLES.upsStatus) || ''),
|
||||
batteryCharge: parseFloat(vars.get(NUT_VARIABLES.batteryCharge) || '0'),
|
||||
batteryRuntime: parseFloat(vars.get(NUT_VARIABLES.batteryRuntime) || '0'),
|
||||
inputVoltage: parseFloat(vars.get(NUT_VARIABLES.inputVoltage) || '0'),
|
||||
outputVoltage: parseFloat(vars.get(NUT_VARIABLES.outputVoltage) || '0'),
|
||||
load: parseFloat(vars.get(NUT_VARIABLES.upsLoad) || '0'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device information
|
||||
*/
|
||||
public async getDeviceInfo(upsName: string): Promise<{
|
||||
manufacturer: string;
|
||||
model: string;
|
||||
serial: string;
|
||||
type: string;
|
||||
}> {
|
||||
const vars = await this.getVariables(upsName, [
|
||||
NUT_VARIABLES.deviceMfr,
|
||||
NUT_VARIABLES.deviceModel,
|
||||
NUT_VARIABLES.deviceSerial,
|
||||
NUT_VARIABLES.deviceType,
|
||||
]);
|
||||
|
||||
return {
|
||||
manufacturer: vars.get(NUT_VARIABLES.deviceMfr) || '',
|
||||
model: vars.get(NUT_VARIABLES.deviceModel) || '',
|
||||
serial: vars.get(NUT_VARIABLES.deviceSerial) || '',
|
||||
type: vars.get(NUT_VARIABLES.deviceType) || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure connected before command
|
||||
*/
|
||||
private async ensureConnected(): Promise<void> {
|
||||
if (!this.connected) {
|
||||
await this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a NUT server is reachable
|
||||
*/
|
||||
public static async probe(address: string, port: number = 3493, timeout: number = 3000): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new plugins.net.Socket();
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
}, timeout);
|
||||
|
||||
socket.on('connect', () => {
|
||||
clearTimeout(timer);
|
||||
socket.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
socket.on('error', () => {
|
||||
clearTimeout(timer);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
socket.connect(port, address);
|
||||
});
|
||||
}
|
||||
}
|
||||
548
ts/ups/ups.classes.upsdevice.ts
Normal file
548
ts/ups/ups.classes.upsdevice.ts
Normal file
@@ -0,0 +1,548 @@
|
||||
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<void> {
|
||||
if (this.upsProtocol === 'nut') {
|
||||
await this.connectNut();
|
||||
} else {
|
||||
await this.connectSnmp();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect via NUT protocol
|
||||
*/
|
||||
private async connectNut(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<IUpsBatteryInfo> {
|
||||
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<TUpsBatteryStatus, IUpsBatteryInfo['status']> = {
|
||||
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<IUpsPowerInfo> {
|
||||
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<IUpsFullStatus> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
const command = type === 'deep'
|
||||
? NUT_COMMANDS.testBatteryStartDeep
|
||||
: NUT_COMMANDS.testBatteryStartQuick;
|
||||
return this.runCommand(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop battery test
|
||||
*/
|
||||
public async stopBatteryTest(): Promise<boolean> {
|
||||
return this.runCommand(NUT_COMMANDS.testBatteryStop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle beeper
|
||||
*/
|
||||
public async toggleBeeper(): Promise<boolean> {
|
||||
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';
|
||||
Reference in New Issue
Block a user