feat(unifi): implement comprehensive UniFi API client with controllers, protect, access, account, managers, resources, HTTP client, interfaces, logging, plugins, and tests
This commit is contained in:
262
ts/classes.clientmanager.ts
Normal file
262
ts/classes.clientmanager.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
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<UnifiClient[]> {
|
||||
logger.log('debug', `Fetching active clients for site: ${siteId}`);
|
||||
|
||||
const response = await this.controller.request<IUnifiApiResponse<INetworkClient>>(
|
||||
'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<UnifiClient[]> {
|
||||
logger.log('debug', `Fetching all known clients for site: ${siteId}`);
|
||||
|
||||
const response = await this.controller.request<IUnifiApiResponse<INetworkClient>>(
|
||||
'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<UnifiClient | null> {
|
||||
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<UnifiClient | null> {
|
||||
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<UnifiClient | null> {
|
||||
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<UnifiClient[]> {
|
||||
const clients = await this.listActiveClients(siteId);
|
||||
return clients.filter((client) => client.is_wired);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get wireless clients only
|
||||
*/
|
||||
public async getWirelessClients(siteId: string = 'default'): Promise<UnifiClient[]> {
|
||||
const clients = await this.listActiveClients(siteId);
|
||||
return clients.filter((client) => client.isWireless());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get guest clients only
|
||||
*/
|
||||
public async getGuestClients(siteId: string = 'default'): Promise<UnifiClient[]> {
|
||||
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<UnifiClient[]> {
|
||||
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<UnifiClient[]> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<UnifiClient[]> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user