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:
276
ts/classes.client.ts
Normal file
276
ts/classes.client.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user