import type { UnifiController } from './classes.unifi-controller.js'; import type { INetworkClient } from './interfaces/index.js'; /** * Represents a connected network client */ export class UnifiClient implements INetworkClient { /** Reference to parent controller */ private controller?: UnifiController; /** Site ID for API calls */ private siteId?: string; // INetworkClient properties public _id: string; public mac: string; public site_id: string; public is_guest?: boolean; public is_wired: boolean; public first_seen?: number; public last_seen?: number; public hostname?: string; public name?: string; public ip?: string; public network_id?: string; public uplink_mac?: string; public ap_name?: string; public essid?: string; public bssid?: string; public channel?: number; public radio_proto?: string; public signal?: number; public tx_rate?: number; public rx_rate?: number; public tx_bytes?: number; public rx_bytes?: number; public tx_packets?: number; public rx_packets?: number; public sw_port?: number; public usergroup_id?: string; public oui?: string; public noted?: boolean; public user_id?: string; public fingerprint_source?: number; public dev_cat?: number; public dev_family?: number; public dev_vendor?: number; public dev_id?: number; public os_name?: number; public satisfaction?: number; public anomalies?: number; constructor() { this._id = ''; this.mac = ''; this.site_id = ''; this.is_wired = false; } /** * Create a client instance from API response object */ public static createFromApiObject( apiObject: INetworkClient, controller?: UnifiController, siteId?: string ): UnifiClient { const client = new UnifiClient(); Object.assign(client, apiObject); client.controller = controller; client.siteId = siteId || apiObject.site_id; return client; } /** * Get the raw API object representation */ public toApiObject(): INetworkClient { return { _id: this._id, mac: this.mac, site_id: this.site_id, is_guest: this.is_guest, is_wired: this.is_wired, first_seen: this.first_seen, last_seen: this.last_seen, hostname: this.hostname, name: this.name, ip: this.ip, network_id: this.network_id, uplink_mac: this.uplink_mac, ap_name: this.ap_name, essid: this.essid, bssid: this.bssid, channel: this.channel, radio_proto: this.radio_proto, signal: this.signal, tx_rate: this.tx_rate, rx_rate: this.rx_rate, tx_bytes: this.tx_bytes, rx_bytes: this.rx_bytes, tx_packets: this.tx_packets, rx_packets: this.rx_packets, sw_port: this.sw_port, usergroup_id: this.usergroup_id, oui: this.oui, noted: this.noted, user_id: this.user_id, fingerprint_source: this.fingerprint_source, dev_cat: this.dev_cat, dev_family: this.dev_family, dev_vendor: this.dev_vendor, dev_id: this.dev_id, os_name: this.os_name, satisfaction: this.satisfaction, anomalies: this.anomalies, }; } /** * Get display name (name, hostname, or MAC) */ public getDisplayName(): string { return this.name || this.hostname || this.mac; } /** * Check if client is wireless */ public isWireless(): boolean { return !this.is_wired; } /** * Check if client is a guest */ public isGuest(): boolean { return this.is_guest === true; } /** * Get connection type string */ public getConnectionType(): string { if (this.is_wired) { return `Wired (Port ${this.sw_port || 'unknown'})`; } return `Wireless (${this.essid || 'unknown'})`; } /** * Get signal strength description */ public getSignalQuality(): string { if (this.is_wired) return 'N/A'; if (!this.signal) return 'Unknown'; // Signal is typically in dBm, with higher (less negative) being better const signal = this.signal; if (signal >= -50) return 'Excellent'; if (signal >= -60) return 'Good'; if (signal >= -70) return 'Fair'; if (signal >= -80) return 'Poor'; return 'Very Poor'; } /** * Block this client from the network */ public async block(): Promise { if (!this.controller || !this.siteId) { throw new Error('Cannot block client: no controller reference'); } await this.controller.request( 'POST', `/api/s/${this.siteId}/cmd/stamgr`, { cmd: 'block-sta', mac: this.mac, } ); } /** * Unblock this client */ public async unblock(): Promise { if (!this.controller || !this.siteId) { throw new Error('Cannot unblock client: no controller reference'); } await this.controller.request( 'POST', `/api/s/${this.siteId}/cmd/stamgr`, { cmd: 'unblock-sta', mac: this.mac, } ); } /** * Reconnect (kick) this client */ public async reconnect(): Promise { if (!this.controller || !this.siteId) { throw new Error('Cannot reconnect client: no controller reference'); } await this.controller.request( 'POST', `/api/s/${this.siteId}/cmd/stamgr`, { cmd: 'kick-sta', mac: this.mac, } ); } /** * Rename this client */ public async rename(newName: string): Promise { if (!this.controller || !this.siteId) { throw new Error('Cannot rename client: no controller reference'); } // Check if user exists (has fixed IP/config) if (this.user_id) { await this.controller.request( 'PUT', `/api/s/${this.siteId}/rest/user/${this.user_id}`, { name: newName, } ); } else { // Create user entry for this client await this.controller.request( 'POST', `/api/s/${this.siteId}/rest/user`, { mac: this.mac, name: newName, } ); } this.name = newName; } /** * Get data usage (combined TX and RX bytes) */ public getDataUsage(): number { return (this.tx_bytes || 0) + (this.rx_bytes || 0); } /** * Format data usage as human-readable string */ public getDataUsageFormatted(): string { const bytes = this.getDataUsage(); if (bytes >= 1024 * 1024 * 1024) { return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; } if (bytes >= 1024 * 1024) { return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; } if (bytes >= 1024) { return `${(bytes / 1024).toFixed(2)} KB`; } return `${bytes} B`; } }