import type { UnifiController } from './classes.unifi-controller.js'; import { UnifiClient } from './classes.client.js'; import type { INetworkClient, IUnifiApiResponse } from './interfaces/index.js'; import { logger } from './unifi.logger.js'; /** * Manager for UniFi network clients */ export class ClientManager { private controller: UnifiController; constructor(controller: UnifiController) { this.controller = controller; } /** * List all active (connected) clients for a site */ public async listActiveClients(siteId: string = 'default'): Promise { logger.log('debug', `Fetching active clients for site: ${siteId}`); const response = await this.controller.request>( 'GET', `/api/s/${siteId}/stat/sta` ); const clients: UnifiClient[] = []; for (const clientData of response.data || []) { clients.push(UnifiClient.createFromApiObject(clientData, this.controller, siteId)); } logger.log('info', `Found ${clients.length} active clients`); return clients; } /** * List all known clients (including historical) */ public async listAllClients(siteId: string = 'default'): Promise { logger.log('debug', `Fetching all known clients for site: ${siteId}`); const response = await this.controller.request>( 'GET', `/api/s/${siteId}/rest/user` ); const clients: UnifiClient[] = []; for (const clientData of response.data || []) { clients.push(UnifiClient.createFromApiObject(clientData, this.controller, siteId)); } logger.log('info', `Found ${clients.length} known clients`); return clients; } /** * Get a client by MAC address */ public async getClientByMac(mac: string, siteId: string = 'default'): Promise { const normalizedMac = mac.toLowerCase().replace(/[:-]/g, ':'); const clients = await this.listActiveClients(siteId); return clients.find((client) => client.mac.toLowerCase() === normalizedMac) || null; } /** * Get a client by IP address */ public async getClientByIp(ip: string, siteId: string = 'default'): Promise { const clients = await this.listActiveClients(siteId); return clients.find((client) => client.ip === ip) || null; } /** * Get a client by hostname */ public async getClientByHostname(hostname: string, siteId: string = 'default'): Promise { const clients = await this.listActiveClients(siteId); return clients.find( (client) => client.hostname?.toLowerCase() === hostname.toLowerCase() ) || null; } /** * Get wired clients only */ public async getWiredClients(siteId: string = 'default'): Promise { const clients = await this.listActiveClients(siteId); return clients.filter((client) => client.is_wired); } /** * Get wireless clients only */ public async getWirelessClients(siteId: string = 'default'): Promise { const clients = await this.listActiveClients(siteId); return clients.filter((client) => client.isWireless()); } /** * Get guest clients only */ public async getGuestClients(siteId: string = 'default'): Promise { const clients = await this.listActiveClients(siteId); return clients.filter((client) => client.isGuest()); } /** * Get clients connected to a specific AP */ public async getClientsByAP(apMac: string, siteId: string = 'default'): Promise { const normalizedMac = apMac.toLowerCase(); const clients = await this.listActiveClients(siteId); return clients.filter((client) => client.uplink_mac?.toLowerCase() === normalizedMac); } /** * Get clients on a specific SSID */ public async getClientsBySSID(ssid: string, siteId: string = 'default'): Promise { const clients = await this.listActiveClients(siteId); return clients.filter((client) => client.essid === ssid); } /** * Block a client by MAC address */ public async blockClient(mac: string, siteId: string = 'default'): Promise { logger.log('info', `Blocking client: ${mac}`); await this.controller.request( 'POST', `/api/s/${siteId}/cmd/stamgr`, { cmd: 'block-sta', mac: mac.toLowerCase(), } ); } /** * Unblock a client by MAC address */ public async unblockClient(mac: string, siteId: string = 'default'): Promise { logger.log('info', `Unblocking client: ${mac}`); await this.controller.request( 'POST', `/api/s/${siteId}/cmd/stamgr`, { cmd: 'unblock-sta', mac: mac.toLowerCase(), } ); } /** * Reconnect (kick) a client by MAC address */ public async reconnectClient(mac: string, siteId: string = 'default'): Promise { logger.log('info', `Reconnecting client: ${mac}`); await this.controller.request( 'POST', `/api/s/${siteId}/cmd/stamgr`, { cmd: 'kick-sta', mac: mac.toLowerCase(), } ); } /** * Authorize a guest client */ public async authorizeGuest( mac: string, minutes: number = 60, options: { up?: number; // Upload bandwidth limit in Kbps down?: number; // Download bandwidth limit in Kbps bytes?: number; // Data quota in MB apMac?: string; // Specific AP MAC } = {}, siteId: string = 'default' ): Promise { logger.log('info', `Authorizing guest: ${mac} for ${minutes} minutes`); await this.controller.request( 'POST', `/api/s/${siteId}/cmd/stamgr`, { cmd: 'authorize-guest', mac: mac.toLowerCase(), minutes, up: options.up, down: options.down, bytes: options.bytes, ap_mac: options.apMac?.toLowerCase(), } ); } /** * Unauthorize a guest client */ public async unauthorizeGuest(mac: string, siteId: string = 'default'): Promise { logger.log('info', `Unauthorizing guest: ${mac}`); await this.controller.request( 'POST', `/api/s/${siteId}/cmd/stamgr`, { cmd: 'unauthorize-guest', mac: mac.toLowerCase(), } ); } /** * Set or update client name/alias */ public async setClientName(mac: string, name: string, siteId: string = 'default'): Promise { logger.log('info', `Setting client name: ${mac} -> ${name}`); // First try to find existing user const allClients = await this.listAllClients(siteId); const existingClient = allClients.find( (c) => c.mac.toLowerCase() === mac.toLowerCase() ); if (existingClient?.user_id) { // Update existing user await this.controller.request( 'PUT', `/api/s/${siteId}/rest/user/${existingClient.user_id}`, { name } ); } else { // Create new user entry await this.controller.request( 'POST', `/api/s/${siteId}/rest/user`, { mac: mac.toLowerCase(), name, } ); } } /** * Get blocked clients */ public async getBlockedClients(siteId: string = 'default'): Promise { logger.log('debug', `Fetching blocked clients for site: ${siteId}`); // Blocked clients are in the user endpoint with blocked=true const allClients = await this.listAllClients(siteId); // Note: This filtering may need adjustment based on actual API response return allClients.filter((client) => (client as any).blocked === true); } }