import net, { Socket } from 'node:net' import { appendLog } from '../ultils/helper.js' interface APCOptions { host: string port?: number username: string password: string onData?: (data: string, status: string) => void number?: number keep_connect?: boolean } interface PromptCallback { prompt: string callback: (data: string) => void } class APCController { public apc_number?: number public apc_ip: string private apc_port: number private apc_username: string private apc_password: string public status: 'CONNECTED' | 'DISCONNECTED' | 'TIMEOUT' private socket: Socket private buffer: string public output: string private promptCallbacks: PromptCallback[] private onData: (data: string, status: string) => void private retryConnect: number constructor({ host, port = 23, username, password, onData, number }: APCOptions) { this.apc_number = number this.apc_ip = host this.apc_port = port this.apc_username = username this.apc_password = password this.status = 'DISCONNECTED' this.socket = new net.Socket() this.buffer = '' this.output = '... Starting ...\n' this.promptCallbacks = [] this.onData = onData || (() => {}) this.retryConnect = 0 } private sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } public async connect(): Promise { try { return new Promise((resolve, reject) => { this.socket.connect(this.apc_port, this.apc_ip, () => { this.status = 'CONNECTED' // this.retryConnect = 0 this.socket.setEncoding('utf8') resolve() }) this.socket.on('data', (data) => this._handleData(data.toString())) this.socket.on('close', () => { this._handleClose() resolve() }) this.socket.on('timeout', () => { this._handleTimeout() resolve() }) this.socket.on('error', (err) => { this._handleError(err) resolve() }) }) } catch (e) { console.error(e) } } public disconnect(): void { if (this.socket && !this.socket.destroyed) { this.socket.removeAllListeners() this.socket.destroy() this.socket.unref() } } private _handleData(data: string): void { this.output += data this.output = this.output.slice(-10000) this.buffer += data this.buffer = this.buffer.slice(-10000) this.onData(this.buffer, this.status) if (this.promptCallbacks.length > 0) { const { prompt, callback } = this.promptCallbacks[0] if (this.buffer.includes(prompt)) { const cb = this.promptCallbacks.shift() if (cb) cb.callback(this.buffer) this.buffer = '' } } // appendLog(data, 0, 0, this.apc_number || 0) } private _handleClose(): void { this.status = 'DISCONNECTED' this.output += '\r\n\r\n[DISCONNECTED] Socket closed' this.onData(this.output, this.status) this._cleanup() } private async _handleTimeout(): Promise { this.status = 'TIMEOUT' this.output += '\r\n\r\n[TIMEOUT] Connection timed out' this.onData(this.output, this.status) if (this.retryConnect <= 5) { await this.sleep(5000) console.log('Retry connect times', this.retryConnect) this.retryConnect += 1 await this.reconnect() } else { this.retryConnect = 0 } } private async _handleError(err: NodeJS.ErrnoException): Promise { this.output += `\r\n\r\n[ERROR] ${err.message}` this.onData(this.output, this.status) if (err.code === 'ECONNRESET') { console.log('[ECONNRESET] Trying reconnect apc:', this.apc_ip) if (this.retryConnect <= 5) { await this.sleep(15000) console.log('Retry connect times', this.retryConnect) this.retryConnect += 1 await this.reconnect() } } } private _waitFor(prompt: string, timeout = 5000): Promise { return new Promise((resolve, reject) => { const timer = setTimeout(() => { this._handleTimeout() reject(new Error(`Timeout waiting for: ${prompt}`)) }, timeout) this.promptCallbacks.push({ prompt, callback: (data) => { clearTimeout(timer) resolve(data) }, }) }) } public _send(command: string): void { if (this.socket && !this.socket.destroyed && this.socket.readyState) { // console.log('SEND COMMAND TO APC:', command) this.socket.write(this._convertSpecialKey(command) + '\r\n') } } private _cleanup(): void { this.promptCallbacks = [] this.socket.removeAllListeners() this.socket.unref() this.socket.destroy() } private _convertSpecialKey(key: string): string { switch (key) { case 'ENTER': return '' case 'ESC': return '\x1B' case 'CTRL-L': return '\x0C' case 'SPACE': return ' ' case 'D': return '\x44' default: return key } } public async login(): Promise { await this.sleep(500) this._send(this.apc_username) await this.sleep(500) this._send(this.apc_password) await this.sleep(1000) this._send('1') await this.sleep(5000) this._send('ENTER') } public async returnToMainMenu(maxAttempts = 5): Promise { for (let i = 0; i < maxAttempts; i++) { this._send('\x1B') // const menuText = await this._waitFor('Main Menu', 5000) // if (menuText.includes('Control Console') || menuText.includes('Device Manager')) { // return // } } // throw new Error('Unable to return to main menu after ESC attempts') } public async navigateToOutlets(): Promise { await this.returnToMainMenu() this._send('1') } public async navigateToOutlet(outletNumber: number): Promise { await this.returnToMainMenu() this._send('1') await this.sleep(500) this._send(outletNumber.toString()) await this.sleep(500) this._send('1') } public async turnOnOutlet(outletNumber: number): Promise { await this.navigateToOutlet(outletNumber) this._send('1') await this.sleep(500) this._send('YES') await this.sleep(500) this._send('') await this.sleep(500) this._send('\x1B') await this.sleep(500) this._send('\x1B') await this.sleep(2000) this._send('') } public async turnOffOutlet(outletNumber: number): Promise { await this.navigateToOutlet(outletNumber) this._send('2') await this.sleep(500) this._send('YES') await this.sleep(500) this._send('') await this.sleep(500) this._send('\x1B') await this.sleep(500) this._send('\x1B') await this.sleep(2000) this._send('') } public async restartOutlet(outletNumber: number): Promise { await this.navigateToOutlet(outletNumber) this._send('3') await this.sleep(500) this._send('YES') await this.sleep(500) this._send('') await this.sleep(500) this._send('\x1B') await this.sleep(500) this._send('\x1B') await this.sleep(2000) this._send('') } public async reconnect(): Promise { try { this.disconnect() await this.sleep(1000) console.log('RECONNECT APC:', this.apc_number, 'IP:', this.apc_ip) this.socket = new net.Socket() await this.connect() await this.login() return true } catch (err) { this._handleError(err as NodeJS.ErrnoException) return false } } } export default APCController