import net from 'node:net' type PromptCallback = { prompt: string callback: (data: string) => void } type PortInfo = { name: string status: string poe: string } interface SwitchControllerOptions { host: string port?: number username: string password: string onData?: (data?: PortInfo[][], status?: string) => void keep_connect?: boolean } export default class SwitchController { private host: string private port: number private username: string private password: string private onData: (data?: PortInfo[][], status?: string) => void private socket: net.Socket public status: 'CONNECTED' | 'DISCONNECTED' public buffer: string private promptCallbacks: PromptCallback[] public ports: PortInfo[] public portGroups: PortInfo[][] private isEnable: boolean private retryConnect: number constructor({ host, port = 23, username, password, onData }: SwitchControllerOptions) { this.host = host this.port = port this.username = username this.password = password this.onData = onData || (() => {}) this.socket = new net.Socket() this.status = 'DISCONNECTED' this.buffer = '' this.promptCallbacks = [] this.ports = [] this.portGroups = [] this.isEnable = false this.retryConnect = 0 } private sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } private _handleData(data: string) { this.buffer += data if (this.promptCallbacks.length > 0) { const { prompt, callback } = this.promptCallbacks[0] if (this.buffer.includes(prompt)) { this.promptCallbacks.shift()?.callback(this.buffer) this.buffer = '' } } } private async _handleClose(err: boolean) { console.log('[SWITCH CONNECTION CLOSE]', err) this.status = 'DISCONNECTED' this.isEnable = false this.onData(this.portGroups, this.status) if (this.retryConnect <= 5) { await this.sleep(15000) console.log('Retry connect times', this.retryConnect) this.retryConnect += 1 await this.reconnect() } else { this.retryConnect = 0 } } private _handleError(err: Error & { code?: string }) { console.log('[SWITCH CONNECTION ERROR]:', err.message) this.onData(this.portGroups, this.status) } private _handleTimeout() { this.onData(this.portGroups, this.status) } private _waitFor(prompt: string, timeout = 5000): Promise { return new Promise((resolve, reject) => { const timer = setTimeout(() => { resolve(`Timeout waiting for: ${prompt}`) }, timeout) this.promptCallbacks.push({ prompt, callback: (data: string) => { clearTimeout(timer) resolve(data) }, }) }) } private _send(command: string) { if (this.socket && !this.socket.destroyed && this.status === 'CONNECTED') { this.socket.write(command + '\r\n') } } public connect(): Promise { return new Promise((resolve, reject) => { try { this.socket.connect(this.port, this.host, () => { this.status = 'CONNECTED' // this.retryConnect = 0 this.isEnable = false this.socket.setEncoding('utf8') this.checkStatusAllPorts() this.socket.on('data', (data) => this._handleData(data.toString())) resolve() }) this.socket.on('close', (e) => { this._handleClose(e) resolve() }) this.socket.on('error', (err) => { this._handleError(err) resolve() }) this.socket.on('timeout', () => { this._handleTimeout() resolve() }) } catch (error: any) { console.log('[ERROR CONNECT SWITCH]:', error.message) reject(error) } }) } public disconnect() { this.socket.end() this.status = 'DISCONNECTED' this.isEnable = false } public checkStatusAllPorts() { const interval = setInterval(async () => { try { if (this.status !== 'CONNECTED') { clearInterval(interval) return } await this.getPorts() if (this.promptCallbacks.length === 0) this.buffer = '' } catch (err: any) { console.error('Error checking port status:', err) this.onData(this.portGroups, this.status) } }, 5000) } public async enterEnableMode() { if (this.isEnable) return await this.sleep(1000) this._send('') await this.sleep(1000) this._send('') await this.sleep(1000) this._send('enable') await this.sleep(1000) this._send(this.password) this.isEnable = true } public async login() { await this.enterEnableMode() this._send('terminal length 0') } public async checkPortStatus(port: string) { this._send(`show interface ${port}`) const result = await this._waitFor('#') return result } public async checkPoEStatus(port: string) { this._send(`show power inline ${port}`) const result = await this._waitFor('#') return result } public async turnPortOff(port: string) { await this.enterEnableMode() this._send(`configure terminal`) // await this._waitFor('(config)#') await this.sleep(500) this._send(`interface ${port}`) // await this._waitFor('(config-if)#') await this.sleep(500) this._send(`shutdown`) // await this._waitFor('(config-if)#') await this.sleep(500) this._send(`end`) } public async turnPortOn(port: string) { await this.enterEnableMode() this._send(`configure terminal`) // await this._waitFor('(config)#') await this.sleep(500) this._send(`interface ${port}`) // await this._waitFor('(config-if)#') await this.sleep(500) this._send(`no shutdown`) // await this._waitFor('(config-if)#') await this.sleep(500) this._send(`end`) } public async restartPort(port: string) { // await this.enterEnableMode() // this._send(`configure terminal`) // // await this._waitFor('(config)#') // await this.sleep(500) // this._send(`interface ${port}`) // // await this._waitFor('(config-if)#') // await this.sleep(500) // this._send(`shutdown`) // // await this._waitFor('(config-if)#') // await this.sleep(500) // await this.sleep(2000) // this._send(`no shutdown`) // // await this._waitFor('(config-if)#') // await this.sleep(500) // this._send(`end`) await this.turnPortOff(port) await this.sleep(300) await this.getPorts() await this.sleep(300) await this.turnPortOn(port) } public async disablePoE(port: string) { await this.enterEnableMode() this._send(`configure terminal`) // await this._waitFor('(config)#') await this.sleep(500) this._send(`interface ${port}`) // await this._waitFor('(config-if)#') await this.sleep(500) this._send(`power inline never`) // await this._waitFor('(config-if)#') await this.sleep(500) this._send(`end`) } public async enablePoE(port: string) { await this.enterEnableMode() this._send(`configure terminal`) // await this._waitFor('(config)#') await this.sleep(500) this._send(`interface ${port}`) // await this._waitFor('(config-if)#') await this.sleep(500) this._send(`power inline auto`) // await this._waitFor('(config-if)#') await this.sleep(500) this._send(`end`) } public async restartPoE(port: string) { await this.disablePoE(port) await this.sleep(3000) await this.enablePoE(port) } public async getPorts(): Promise { this._send(' terminal length 0') this._send('show interface') this._send(' ') await this.sleep(2000) const statusOutput = this.buffer this.buffer = '' const lines = statusOutput.split('\n') const ports = this.ports?.length > 0 ? [...this.ports] : [] for (const line of lines) { // Match: "Gi0/1 is up, line protocol is up" const match = line.match( /^(TenGigabitEthernet|GigabitEthernet|FastEthernet|Ethernet)\S*\s+is\s+(.+?),\s+line protocol is\s+(\S+)/i ) if (match) { const name = match[1] + line.split(' ')[0].replace(match[1], '') const status1 = match[2].toLowerCase() // up / down / administratively const status2 = match[3].toLowerCase() // up / down // Rule: interface is considered ON only when both are "up" const status = status1 === 'up' && status2 === 'up' ? 'ON' : 'OFF' const port = ports.find((p) => p.name === name) if (port) { port.status = status } else { ports.push({ name, status, poe: 'UNKNOWN' }) } } } // PoE check this._send('show power inline') await this.sleep(2000) const poeOutput = this.buffer this.buffer = '' const poeLines = poeOutput.split('\n') for (const line of poeLines) { const match = line.match(/^(\S+)\s+\S+\s+(on|off)/i) if (match) { const name = match[1] const poeStatus = match[2].toLowerCase() === 'on' ? 'ON' : 'OFF' const port = ports.find((p) => p.name === name) if (port) port.poe = poeStatus } } const grouped: Record = {} for (const port of ports) { const prefixMatch = port.name.match(/^([A-Za-z]+)/) const prefix = prefixMatch ? prefixMatch[1] : 'Unknown' if (!grouped[prefix]) grouped[prefix] = [] const existing = grouped[prefix].find((el) => el.name === port.name) if (!existing) grouped[prefix].push(port) } const groupedArray = Object.values(grouped) this.ports = ports this.portGroups = groupedArray?.sort((a, b) => b.length - a.length) this.onData(this.portGroups, this.status) return true } public async reconnect(): Promise { try { this.disconnect() this.socket = new net.Socket() await this.sleep(1000) await this.connect() await this.login() await this.getPorts() return true } catch (err: any) { this._handleError(err) return false } } }