285 lines
7.3 KiB
TypeScript
285 lines
7.3 KiB
TypeScript
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<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
}
|
|
|
|
public async connect(): Promise<void> {
|
|
try {
|
|
return new Promise((resolve, reject) => {
|
|
this.socket.connect(this.apc_port, this.apc_ip, () => {
|
|
this.status = 'CONNECTED'
|
|
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<void> {
|
|
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()
|
|
}
|
|
}
|
|
|
|
private _handleError(err: NodeJS.ErrnoException): void {
|
|
this.output += `\r\n\r\n[ERROR] ${err.message}`
|
|
this.onData(this.output, this.status)
|
|
if (err.code === 'ECONNRESET') {
|
|
setTimeout(() => {
|
|
console.log('[ECONNRESET] Trying reconnect apc:', this.apc_ip)
|
|
this.reconnect()
|
|
}, 15000)
|
|
}
|
|
}
|
|
|
|
private _waitFor(prompt: string, timeout = 5000): Promise<string> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
await this.returnToMainMenu()
|
|
this._send('1')
|
|
}
|
|
|
|
public async navigateToOutlet(outletNumber: number): Promise<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<boolean> {
|
|
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
|