setup unifi wlans
This commit is contained in:
@@ -0,0 +1,952 @@
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import https from "https";
|
||||
import {
|
||||
ApGroup,
|
||||
ApRadioWifiUsage,
|
||||
ApWifiLimits,
|
||||
CreateSiteOptions,
|
||||
Device,
|
||||
DeviceRadio,
|
||||
DeviceState,
|
||||
DeviceUplink,
|
||||
Network,
|
||||
PrivatePSK,
|
||||
PrivatePSKCreateInput,
|
||||
SiteListItem,
|
||||
SiteOverview,
|
||||
SubsystemHealth,
|
||||
SysInfo,
|
||||
UserGroup,
|
||||
UserGroupCreateInput,
|
||||
WlanConf,
|
||||
WlanConfRaw,
|
||||
WlanConfUpdate,
|
||||
WlanGroup,
|
||||
WlanGroupCreateInput,
|
||||
} from "./unifiTypes";
|
||||
|
||||
export class UnifiClient {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor(baseURL: string) {
|
||||
this.client = axios.create({
|
||||
baseURL,
|
||||
validateStatus: (s) => s >= 200 && s < 400,
|
||||
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
|
||||
});
|
||||
}
|
||||
|
||||
private persistSession(res: { headers: Record<string, unknown> }): void {
|
||||
// Cookies
|
||||
const raw = res.headers["set-cookie"];
|
||||
if (raw) {
|
||||
const cookies = (Array.isArray(raw) ? raw : [raw]) as string[];
|
||||
const cookieString = cookies.map((c) => c.split(";")[0]).join("; ");
|
||||
this.client.defaults.headers.common["Cookie"] = cookieString;
|
||||
}
|
||||
// CSRF token (UniFi OS)
|
||||
const csrf = res.headers["x-csrf-token"];
|
||||
if (typeof csrf === "string") {
|
||||
this.client.defaults.headers.common["X-CSRF-Token"] = csrf;
|
||||
}
|
||||
}
|
||||
|
||||
async login(username: string, password: string): Promise<void> {
|
||||
const body = { username, password };
|
||||
|
||||
try {
|
||||
// UniFi OS
|
||||
const res = await this.client.post("/api/auth/login", body);
|
||||
console.log("Login OK (UniFi OS)", res.status);
|
||||
this.persistSession(res);
|
||||
} catch (e) {
|
||||
// Legacy controller
|
||||
console.log("UniFi OS login failed, trying legacy...");
|
||||
const res = await this.client.post("/api/login", body);
|
||||
console.log("Login OK (legacy)", res.status);
|
||||
this.persistSession(res);
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchWlanConfRaw(site: string): Promise<WlanConfRaw[]> {
|
||||
const paths = [
|
||||
`/proxy/network/api/s/${site}/rest/wlanconf`,
|
||||
`/api/s/${site}/rest/wlanconf`,
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.get(path);
|
||||
const data = (res.data?.data ?? res.data) as WlanConfRaw[];
|
||||
console.log(`Fetched wlan from ${path}`);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`Failed ${path}:`,
|
||||
axios.isAxiosError(e) ? e.response?.status : e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not fetch WLAN config from any known path");
|
||||
}
|
||||
|
||||
private static parseWlanConf(w: any): WlanConf {
|
||||
return {
|
||||
id: w._id,
|
||||
name: (w.name || w.ssid || "").toString(),
|
||||
siteId: w.site_id ?? "",
|
||||
enabled: w.enabled ?? true,
|
||||
security: w.security ?? "open",
|
||||
wpaMode: w.wpa_mode ?? "",
|
||||
wpaEnc: w.wpa_enc ?? "",
|
||||
wpa3Support: w.wpa3_support ?? false,
|
||||
wpa3Transition: w.wpa3_transition ?? false,
|
||||
wpa3FastRoaming: w.wpa3_fast_roaming ?? false,
|
||||
wpa3Enhanced192: w.wpa3_enhanced_192 ?? false,
|
||||
passphrase: typeof w.x_passphrase === "string" ? w.x_passphrase : null,
|
||||
passphraseAutogenerated: w.passphrase_autogenerated ?? false,
|
||||
hideSSID: w.hide_ssid ?? false,
|
||||
isGuest: w.is_guest ?? false,
|
||||
band: w.wlan_band ?? "both",
|
||||
bands: w.wlan_bands ?? [],
|
||||
networkconfId: w.networkconf_id ?? "",
|
||||
usergroupId: w.usergroup_id ?? "",
|
||||
apGroupIds: w.ap_group_ids ?? [],
|
||||
apGroupMode: w.ap_group_mode ?? "devices",
|
||||
pmfMode: w.pmf_mode ?? "disabled",
|
||||
groupRekey: w.group_rekey ?? 0,
|
||||
dtimMode: w.dtim_mode ?? "default",
|
||||
dtimNg: w.dtim_ng ?? 1,
|
||||
dtimNa: w.dtim_na ?? 3,
|
||||
dtim6e: w.dtim_6e ?? 3,
|
||||
l2Isolation: w.l2_isolation ?? false,
|
||||
fastRoamingEnabled: w.fast_roaming_enabled ?? false,
|
||||
bssTransition: w.bss_transition ?? false,
|
||||
uapsdEnabled: w.uapsd_enabled ?? false,
|
||||
iappEnabled: w.iapp_enabled ?? false,
|
||||
proxyArp: w.proxy_arp ?? false,
|
||||
mcastenhanceEnabled: w.mcastenhance_enabled ?? false,
|
||||
macFilterEnabled: w.mac_filter_enabled ?? false,
|
||||
macFilterPolicy: w.mac_filter_policy ?? "allow",
|
||||
macFilterList: w.mac_filter_list ?? [],
|
||||
radiusDasEnabled: w.radius_das_enabled ?? false,
|
||||
radiusMacAuthEnabled: w.radius_mac_auth_enabled ?? false,
|
||||
radiusMacaclFormat: w.radius_macacl_format ?? "none_lower",
|
||||
minrateSettingPreference: w.minrate_setting_preference ?? "auto",
|
||||
minrateNgEnabled: w.minrate_ng_enabled ?? false,
|
||||
minrateNgDataRateKbps: w.minrate_ng_data_rate_kbps ?? 1000,
|
||||
minrateNgAdvertisingRates: w.minrate_ng_advertising_rates ?? false,
|
||||
minrateNaEnabled: w.minrate_na_enabled ?? false,
|
||||
minrateNaDataRateKbps: w.minrate_na_data_rate_kbps ?? 6000,
|
||||
minrateNaAdvertisingRates: w.minrate_na_advertising_rates ?? false,
|
||||
settingPreference: w.setting_preference ?? "auto",
|
||||
no2ghzOui: w.no2ghz_oui ?? false,
|
||||
privatePreSharedKeysEnabled: w.private_preshared_keys_enabled ?? false,
|
||||
privatePreSharedKeys: w.private_preshared_keys ?? [],
|
||||
saeGroups: w.sae_groups ?? [],
|
||||
saePsk: w.sae_psk ?? [],
|
||||
schedule: w.schedule ?? [],
|
||||
scheduleWithDuration: w.schedule_with_duration ?? [],
|
||||
bcFilterList: w.bc_filter_list ?? [],
|
||||
externalId: w.external_id ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async getWlanConf(site: string): Promise<WlanConf[]> {
|
||||
const raw = await this.fetchWlanConfRaw(site);
|
||||
return raw.map(UnifiClient.parseWlanConf);
|
||||
}
|
||||
|
||||
async updateWlanConf(
|
||||
site: string,
|
||||
wlanId: string,
|
||||
updates: WlanConfUpdate,
|
||||
): Promise<WlanConf> {
|
||||
const paths = [
|
||||
`/proxy/network/api/s/${site}/rest/wlanconf/${wlanId}`,
|
||||
`/api/s/${site}/rest/wlanconf/${wlanId}`,
|
||||
];
|
||||
|
||||
// Fetch current WLAN to check if a RADIUS profile is configured.
|
||||
// The controller rejects RADIUS fields when no profile is set.
|
||||
const currentWlans = await this.getWlanConf(site);
|
||||
const currentWlan = currentWlans.find((w) => w.id === wlanId);
|
||||
const hasRadius =
|
||||
currentWlan?.security === "wpaeap" || updates.security === "wpaeap";
|
||||
|
||||
if (!hasRadius) {
|
||||
delete updates.radius_das_enabled;
|
||||
delete updates.radius_mac_auth_enabled;
|
||||
}
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.put(path, updates);
|
||||
const raw = (res.data?.data?.[0] ?? res.data) as any;
|
||||
return UnifiClient.parseWlanConf(raw);
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
// Try next path on 404/401, throw on other errors
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
) {
|
||||
throw new Error(
|
||||
`Failed to update WLAN ${wlanId}: ${e.response.status} ${JSON.stringify(e.response.data)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not update WLAN config from any known path");
|
||||
}
|
||||
|
||||
async getAllSites(): Promise<SiteListItem[]> {
|
||||
const paths = ["/proxy/network/api/self/sites", "/api/self/sites"];
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.get(path);
|
||||
const raw = (res.data?.data ?? res.data) as any[];
|
||||
return raw.map(
|
||||
(s: any): SiteListItem => ({
|
||||
id: s._id,
|
||||
name: s.name,
|
||||
description: s.desc ?? "",
|
||||
deviceCount: s.device_count ?? 0,
|
||||
role: s.role ?? "",
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not fetch sites from any known path");
|
||||
}
|
||||
|
||||
async getSiteOverview(site: string): Promise<SiteOverview> {
|
||||
const prefixes = ["/proxy/network", ""];
|
||||
|
||||
for (const prefix of prefixes) {
|
||||
try {
|
||||
const [healthRes, sysInfoRes, sitesRes] = await Promise.all([
|
||||
this.client.get(`${prefix}/api/s/${site}/stat/health`),
|
||||
this.client.get(`${prefix}/api/s/${site}/stat/sysinfo`),
|
||||
this.client.get(`${prefix}/api/self/sites`),
|
||||
]);
|
||||
|
||||
const healthRaw = (healthRes.data?.data ?? healthRes.data) as any[];
|
||||
const sysInfoRaw = (sysInfoRes.data?.data?.[0] ??
|
||||
sysInfoRes.data) as any;
|
||||
const sitesRaw = (sitesRes.data?.data ?? sitesRes.data) as any[];
|
||||
|
||||
const siteRaw = sitesRaw.find((s: any) => s.name === site);
|
||||
if (!siteRaw) throw new Error(`Site "${site}" not found in sites list`);
|
||||
|
||||
const health: SubsystemHealth[] = healthRaw.map((h: any) => ({
|
||||
subsystem: h.subsystem,
|
||||
status: h.status,
|
||||
numUser: h.num_user,
|
||||
numGuest: h.num_guest,
|
||||
numIot: h.num_iot,
|
||||
txBytesR: h["tx_bytes-r"],
|
||||
rxBytesR: h["rx_bytes-r"],
|
||||
numAp: h.num_ap,
|
||||
numSw: h.num_sw,
|
||||
numGw: h.num_gw,
|
||||
numAdopted: h.num_adopted,
|
||||
numDisconnected: h.num_disconnected,
|
||||
numPending: h.num_pending,
|
||||
numDisabled: h.num_disabled,
|
||||
}));
|
||||
|
||||
const sysInfo: SysInfo = {
|
||||
name: sysInfoRaw.name,
|
||||
hostname: sysInfoRaw.hostname,
|
||||
version: sysInfoRaw.version,
|
||||
build: sysInfoRaw.build,
|
||||
timezone: sysInfoRaw.timezone,
|
||||
uptime: sysInfoRaw.uptime,
|
||||
ipAddresses: sysInfoRaw.ip_addrs ?? [],
|
||||
updateAvailable: sysInfoRaw.update_available ?? false,
|
||||
isCloudConsole: sysInfoRaw.is_cloud_console ?? false,
|
||||
dataRetentionDays: sysInfoRaw.data_retention_days ?? 0,
|
||||
informPort: sysInfoRaw.inform_port,
|
||||
httpsPort: sysInfoRaw.https_port,
|
||||
unsupportedDeviceCount: sysInfoRaw.unsupported_device_count ?? 0,
|
||||
};
|
||||
|
||||
return {
|
||||
site: {
|
||||
id: siteRaw._id,
|
||||
name: siteRaw.name,
|
||||
description: siteRaw.desc ?? "",
|
||||
deviceCount: siteRaw.device_count ?? 0,
|
||||
role: siteRaw.role ?? "",
|
||||
},
|
||||
health,
|
||||
sysInfo,
|
||||
};
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not fetch site overview from any known path");
|
||||
}
|
||||
|
||||
private static parseDeviceState(state: number): DeviceState {
|
||||
const map: Record<number, DeviceState> = {
|
||||
0: "disconnected",
|
||||
1: "connected",
|
||||
2: "pending",
|
||||
4: "adopting",
|
||||
5: "adopting",
|
||||
};
|
||||
return map[state] ?? "unknown";
|
||||
}
|
||||
|
||||
async getDevices(site: string): Promise<Device[]> {
|
||||
const paths = [
|
||||
`/proxy/network/api/s/${site}/stat/device`,
|
||||
`/api/s/${site}/stat/device`,
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.get(path);
|
||||
const raw = (res.data?.data ?? res.data) as any[];
|
||||
|
||||
return raw.map((d: any): Device => {
|
||||
const uplink: DeviceUplink | null = d.uplink
|
||||
? {
|
||||
type: d.uplink.type,
|
||||
mac: d.uplink.uplink_mac,
|
||||
ip: d.uplink.uplink_remote_ip,
|
||||
uplinkRemotePort: d.uplink.uplink_remote_port,
|
||||
speed: d.uplink.speed,
|
||||
fullDuplex: d.uplink.full_duplex,
|
||||
}
|
||||
: null;
|
||||
|
||||
const radios: DeviceRadio[] = (d.radio_table ?? []).map(
|
||||
(r: any, i: number) => {
|
||||
const stats = d.radio_table_stats?.[i] ?? {};
|
||||
return {
|
||||
name: r.name ?? r.radio,
|
||||
radio: r.radio,
|
||||
channel: r.channel,
|
||||
txPower: r.tx_power,
|
||||
txPowerMode: r.tx_power_mode,
|
||||
minRssiEnabled: r.min_rssi_enabled ?? false,
|
||||
numSta: stats.num_sta ?? 0,
|
||||
satisfaction: stats.satisfaction ?? null,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
id: d._id,
|
||||
mac: d.mac,
|
||||
ip: d.ip ?? "",
|
||||
name: d.name ?? d.mac,
|
||||
model: d.model,
|
||||
shortname: d.shortname ?? d.model,
|
||||
type: d.type,
|
||||
version: d.version ?? "",
|
||||
serial: d.serial ?? "",
|
||||
state: UnifiClient.parseDeviceState(d.state),
|
||||
adopted: d.adopted ?? false,
|
||||
uptime: d.uptime ?? 0,
|
||||
lastSeen: d.last_seen ?? 0,
|
||||
upgradable: d.upgradable ?? false,
|
||||
satisfaction: d.satisfaction ?? null,
|
||||
numClients: d.num_sta ?? 0,
|
||||
numUserClients: d["user-num_sta"] ?? 0,
|
||||
numGuestClients: d["guest-num_sta"] ?? 0,
|
||||
txBytes: d.tx_bytes ?? 0,
|
||||
rxBytes: d.rx_bytes ?? 0,
|
||||
uplink,
|
||||
radios,
|
||||
modelInLts: d.model_in_lts ?? false,
|
||||
modelInEol: d.model_in_eol ?? false,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not fetch devices from any known path");
|
||||
}
|
||||
|
||||
async getNetworks(site: string): Promise<Network[]> {
|
||||
const paths = [
|
||||
`/proxy/network/api/s/${site}/rest/networkconf`,
|
||||
`/api/s/${site}/rest/networkconf`,
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.get(path);
|
||||
const raw = (res.data?.data ?? res.data) as any[];
|
||||
|
||||
return raw.map(
|
||||
(n: any): Network => ({
|
||||
id: n._id,
|
||||
name: n.name ?? "",
|
||||
purpose: n.purpose ?? "corporate",
|
||||
enabled: n.enabled ?? true,
|
||||
ipSubnet: n.ip_subnet ?? null,
|
||||
vlan: n.vlan != null ? Number(n.vlan) : null,
|
||||
vlanEnabled: n.vlan_enabled ?? false,
|
||||
isNat: n.is_nat ?? false,
|
||||
domainName: n.domain_name ?? null,
|
||||
networkGroup: n.networkgroup ?? null,
|
||||
dhcpdEnabled: n.dhcpd_enabled ?? false,
|
||||
dhcpdStart: n.dhcpd_start ?? null,
|
||||
dhcpdStop: n.dhcpd_stop ?? null,
|
||||
dhcpdLeasetime: n.dhcpd_leasetime ?? null,
|
||||
dhcpRelayEnabled: n.dhcp_relay_enabled ?? false,
|
||||
dhcpGuardEnabled: n.dhcpguard_enabled ?? false,
|
||||
igmpSnooping: n.igmp_snooping ?? false,
|
||||
ipv6Enabled: n.ipv6_enabled ?? false,
|
||||
ipv6InterfaceType: n.ipv6_interface_type ?? null,
|
||||
internetAccessEnabled: n.internet_access_enabled ?? null,
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not fetch networks from any known path");
|
||||
}
|
||||
|
||||
async createSite(description: string): Promise<SiteListItem> {
|
||||
const paths = [
|
||||
"/proxy/network/api/s/default/cmd/sitemgr",
|
||||
"/api/s/default/cmd/sitemgr",
|
||||
];
|
||||
|
||||
const body = { cmd: "add-site", desc: description };
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.post(path, body);
|
||||
const raw = (res.data?.data?.[0] ?? res.data) as any;
|
||||
return {
|
||||
id: raw._id,
|
||||
name: raw.name,
|
||||
description: raw.desc ?? description,
|
||||
deviceCount: raw.device_count ?? 0,
|
||||
role: raw.role ?? "",
|
||||
};
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
) {
|
||||
throw new Error(
|
||||
`Failed to create site: ${e.response.status} ${JSON.stringify(e.response.data)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not create site from any known path");
|
||||
}
|
||||
|
||||
// --- WLAN Groups ---
|
||||
|
||||
async getWlanGroups(site: string): Promise<WlanGroup[]> {
|
||||
const paths = [
|
||||
`/proxy/network/api/s/${site}/rest/wlangroup`,
|
||||
`/api/s/${site}/rest/wlangroup`,
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.get(path);
|
||||
const raw = (res.data?.data ?? res.data) as any[];
|
||||
return raw.map(
|
||||
(g: any): WlanGroup => ({
|
||||
id: g._id,
|
||||
name: g.name ?? "",
|
||||
siteId: g.site_id ?? "",
|
||||
noDelete: g.attr_no_delete ?? false,
|
||||
noEdit: g.attr_no_edit ?? false,
|
||||
hidden: g.attr_hidden ?? false,
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
)
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not fetch WLAN groups from any known path");
|
||||
}
|
||||
|
||||
async createWlanGroup(
|
||||
site: string,
|
||||
input: WlanGroupCreateInput,
|
||||
): Promise<WlanGroup> {
|
||||
const paths = [
|
||||
`/proxy/network/api/s/${site}/rest/wlangroup`,
|
||||
`/api/s/${site}/rest/wlangroup`,
|
||||
];
|
||||
|
||||
const body: Record<string, unknown> = { name: input.name };
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.post(path, body);
|
||||
const raw = (res.data?.data?.[0] ?? res.data) as any;
|
||||
return {
|
||||
id: raw._id,
|
||||
name: raw.name ?? input.name,
|
||||
siteId: raw.site_id ?? "",
|
||||
noDelete: raw.attr_no_delete ?? false,
|
||||
noEdit: raw.attr_no_edit ?? false,
|
||||
hidden: raw.attr_hidden ?? false,
|
||||
};
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
) {
|
||||
throw new Error(
|
||||
`Failed to create WLAN group: ${e.response.status} ${JSON.stringify(e.response.data)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not create WLAN group from any known path");
|
||||
}
|
||||
|
||||
// --- User Groups (Speed Profiles) ---
|
||||
|
||||
async getUserGroups(site: string): Promise<UserGroup[]> {
|
||||
const paths = [
|
||||
`/proxy/network/api/s/${site}/rest/usergroup`,
|
||||
`/api/s/${site}/rest/usergroup`,
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.get(path);
|
||||
const raw = (res.data?.data ?? res.data) as any[];
|
||||
return raw.map(
|
||||
(g: any): UserGroup => ({
|
||||
id: g._id,
|
||||
name: g.name ?? "",
|
||||
siteId: g.site_id ?? "",
|
||||
noDelete: g.attr_no_delete ?? false,
|
||||
downloadLimitKbps: g.qos_rate_max_down ?? -1,
|
||||
uploadLimitKbps: g.qos_rate_max_up ?? -1,
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
)
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not fetch user groups from any known path");
|
||||
}
|
||||
|
||||
async createUserGroup(
|
||||
site: string,
|
||||
input: UserGroupCreateInput,
|
||||
): Promise<UserGroup> {
|
||||
const paths = [
|
||||
`/proxy/network/api/s/${site}/rest/usergroup`,
|
||||
`/api/s/${site}/rest/usergroup`,
|
||||
];
|
||||
|
||||
const body: Record<string, unknown> = { name: input.name };
|
||||
if (input.downloadLimitKbps !== undefined)
|
||||
body.qos_rate_max_down = input.downloadLimitKbps;
|
||||
if (input.uploadLimitKbps !== undefined)
|
||||
body.qos_rate_max_up = input.uploadLimitKbps;
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.post(path, body);
|
||||
const raw = (res.data?.data?.[0] ?? res.data) as any;
|
||||
return {
|
||||
id: raw._id,
|
||||
name: raw.name ?? input.name,
|
||||
siteId: raw.site_id ?? "",
|
||||
noDelete: raw.attr_no_delete ?? false,
|
||||
downloadLimitKbps: raw.qos_rate_max_down ?? -1,
|
||||
uploadLimitKbps: raw.qos_rate_max_up ?? -1,
|
||||
};
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
) {
|
||||
throw new Error(
|
||||
`Failed to create user group: ${e.response.status} ${JSON.stringify(e.response.data)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not create user group from any known path");
|
||||
}
|
||||
|
||||
// --- AP Groups ---
|
||||
|
||||
async getApGroups(site: string): Promise<ApGroup[]> {
|
||||
const paths = [
|
||||
`/proxy/network/v2/api/site/${site}/apgroups`,
|
||||
`/v2/api/site/${site}/apgroups`,
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.get(path);
|
||||
const raw = (res.data?.data ?? res.data) as any[];
|
||||
return raw.map(
|
||||
(g: any): ApGroup => ({
|
||||
id: g._id,
|
||||
name: g.name ?? "",
|
||||
deviceMacs: g.device_macs ?? [],
|
||||
noDelete: g.attr_no_delete ?? false,
|
||||
forWlanconf: g.for_wlanconf ?? false,
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
)
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not fetch AP groups from any known path");
|
||||
}
|
||||
|
||||
async createApGroup(
|
||||
site: string,
|
||||
name: string,
|
||||
deviceMacs: string[],
|
||||
forWlanconf: boolean = false,
|
||||
): Promise<ApGroup> {
|
||||
const paths = [
|
||||
`/proxy/network/v2/api/site/${site}/apgroups`,
|
||||
`/v2/api/site/${site}/apgroups`,
|
||||
];
|
||||
|
||||
const body = {
|
||||
name,
|
||||
device_macs: deviceMacs,
|
||||
for_wlanconf: forWlanconf,
|
||||
};
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.post(path, body);
|
||||
const raw = (res.data?.data?.[0] ?? res.data) as any;
|
||||
return {
|
||||
id: raw._id,
|
||||
name: raw.name ?? name,
|
||||
deviceMacs: raw.device_macs ?? deviceMacs,
|
||||
noDelete: raw.attr_no_delete ?? false,
|
||||
forWlanconf: raw.for_wlanconf ?? forWlanconf,
|
||||
};
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
) {
|
||||
throw new Error(
|
||||
`Failed to create AP group: ${e.response.status} ${JSON.stringify(e.response.data)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not create AP group from any known path");
|
||||
}
|
||||
|
||||
async updateApGroup(
|
||||
site: string,
|
||||
groupId: string,
|
||||
deviceMacs: string[],
|
||||
): Promise<ApGroup> {
|
||||
const paths = [
|
||||
`/proxy/network/v2/api/site/${site}/apgroups/${groupId}`,
|
||||
`/v2/api/site/${site}/apgroups/${groupId}`,
|
||||
];
|
||||
|
||||
const body = {
|
||||
name: "devices_ap_group",
|
||||
device_macs: deviceMacs,
|
||||
for_wlanconf: true,
|
||||
};
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.put(path, body);
|
||||
const raw = (res.data?.data?.[0] ?? res.data) as any;
|
||||
return {
|
||||
id: raw._id ?? groupId,
|
||||
name: raw.name ?? "devices_ap_group",
|
||||
deviceMacs: raw.device_macs ?? deviceMacs,
|
||||
noDelete: raw.attr_no_delete ?? false,
|
||||
forWlanconf: raw.for_wlanconf ?? true,
|
||||
};
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
) {
|
||||
throw new Error(
|
||||
`Failed to update AP group: ${e.response.status} ${JSON.stringify(e.response.data)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not update AP group from any known path");
|
||||
}
|
||||
|
||||
// --- Access Points ---
|
||||
|
||||
async getAccessPoints(site: string): Promise<Device[]> {
|
||||
const devices = await this.getDevices(site);
|
||||
return devices.filter((d) => d.type === "uap");
|
||||
}
|
||||
|
||||
// --- WiFi Limits ---
|
||||
|
||||
async getWifiLimits(site: string): Promise<ApWifiLimits[]> {
|
||||
const SSID_LIMIT_PER_RADIO = 8;
|
||||
|
||||
const paths = [
|
||||
`/proxy/network/api/s/${site}/stat/device`,
|
||||
`/api/s/${site}/stat/device`,
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.get(path);
|
||||
const raw = (res.data?.data ?? res.data) as any[];
|
||||
const aps = raw.filter((d: any) => d.type === "uap");
|
||||
|
||||
return aps.map((ap: any): ApWifiLimits => {
|
||||
const vapTable: any[] = ap.vap_table ?? [];
|
||||
const radioMap = new Map<string, { wlanNames: Set<string> }>();
|
||||
|
||||
for (const vap of vapTable) {
|
||||
if (!vap.up || !vap.radio) continue;
|
||||
if (!radioMap.has(vap.radio)) {
|
||||
radioMap.set(vap.radio, { wlanNames: new Set() });
|
||||
}
|
||||
if (vap.essid) {
|
||||
radioMap.get(vap.radio)!.wlanNames.add(vap.essid);
|
||||
}
|
||||
}
|
||||
|
||||
const radioBandMap: Record<string, string> = {
|
||||
ng: "2g",
|
||||
na: "5g",
|
||||
"6e": "6e",
|
||||
};
|
||||
|
||||
const radios: ApRadioWifiUsage[] = Array.from(radioMap.entries()).map(
|
||||
([radio, data]): ApRadioWifiUsage => ({
|
||||
radio,
|
||||
band: radioBandMap[radio] ?? radio,
|
||||
activeWlans: data.wlanNames.size,
|
||||
limit: SSID_LIMIT_PER_RADIO,
|
||||
remaining: Math.max(
|
||||
0,
|
||||
SSID_LIMIT_PER_RADIO - data.wlanNames.size,
|
||||
),
|
||||
wlanNames: Array.from(data.wlanNames),
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
apId: ap._id,
|
||||
apName: ap.name ?? ap.mac,
|
||||
mac: ap.mac,
|
||||
model: ap.model ?? "",
|
||||
radios,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
)
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not fetch WiFi limits from any known path");
|
||||
}
|
||||
|
||||
// --- Private Pre-Shared Keys ---
|
||||
|
||||
private static parsePPSKs(raw: any[]): PrivatePSK[] {
|
||||
return raw.map(
|
||||
(p: any): PrivatePSK => ({
|
||||
key: p.key ?? "",
|
||||
name: p.name ?? "",
|
||||
mac: p.mac ?? null,
|
||||
vlanId: p.vlan_id ?? null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getPrivatePSKs(site: string, wlanId: string): Promise<PrivatePSK[]> {
|
||||
const paths = [
|
||||
`/proxy/network/api/s/${site}/rest/wlanconf/${wlanId}`,
|
||||
`/api/s/${site}/rest/wlanconf/${wlanId}`,
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.get(path);
|
||||
const raw = (res.data?.data?.[0] ?? res.data) as any;
|
||||
return UnifiClient.parsePPSKs(raw.private_preshared_keys ?? []);
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
)
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not fetch PPSKs from any known path");
|
||||
}
|
||||
|
||||
async createPrivatePSK(
|
||||
site: string,
|
||||
wlanId: string,
|
||||
psk: PrivatePSKCreateInput,
|
||||
): Promise<PrivatePSK[]> {
|
||||
const paths = [
|
||||
`/proxy/network/api/s/${site}/rest/wlanconf/${wlanId}`,
|
||||
`/api/s/${site}/rest/wlanconf/${wlanId}`,
|
||||
];
|
||||
|
||||
// Fetch current PPSKs
|
||||
let currentPpsks: any[] = [];
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.get(path);
|
||||
const raw = (res.data?.data?.[0] ?? res.data) as any;
|
||||
currentPpsks = (raw.private_preshared_keys ?? []) as any[];
|
||||
break;
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
)
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const newPsk: Record<string, unknown> = {
|
||||
key: psk.key,
|
||||
name: psk.name,
|
||||
};
|
||||
if (psk.mac) newPsk.mac = psk.mac;
|
||||
if (psk.vlanId !== undefined) newPsk.vlan_id = psk.vlanId;
|
||||
currentPpsks.push(newPsk);
|
||||
|
||||
// Update WLAN with new PPSKs
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const res = await this.client.put(path, {
|
||||
private_preshared_keys: currentPpsks,
|
||||
private_preshared_keys_enabled: true,
|
||||
});
|
||||
const raw = (res.data?.data?.[0] ?? res.data) as any;
|
||||
return UnifiClient.parsePPSKs(raw.private_preshared_keys ?? []);
|
||||
} catch (e) {
|
||||
if (!axios.isAxiosError(e)) throw e;
|
||||
if (
|
||||
e.response &&
|
||||
e.response.status !== 404 &&
|
||||
e.response.status !== 401
|
||||
) {
|
||||
throw new Error(
|
||||
`Failed to create PPSK: ${e.response.status} ${JSON.stringify(e.response.data)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not create PPSK from any known path");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
export interface WlanConfRaw {
|
||||
_id: string;
|
||||
name?: string;
|
||||
ssid?: string;
|
||||
x_passphrase?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WlanConf {
|
||||
id: string;
|
||||
name: string;
|
||||
siteId: string;
|
||||
enabled: boolean;
|
||||
security: string;
|
||||
wpaMode: string;
|
||||
wpaEnc: string;
|
||||
wpa3Support: boolean;
|
||||
wpa3Transition: boolean;
|
||||
wpa3FastRoaming: boolean;
|
||||
wpa3Enhanced192: boolean;
|
||||
passphrase: string | null;
|
||||
passphraseAutogenerated: boolean;
|
||||
hideSSID: boolean;
|
||||
isGuest: boolean;
|
||||
band: string;
|
||||
bands: string[];
|
||||
networkconfId: string;
|
||||
usergroupId: string;
|
||||
apGroupIds: string[];
|
||||
apGroupMode: string;
|
||||
pmfMode: string;
|
||||
groupRekey: number;
|
||||
dtimMode: string;
|
||||
dtimNg: number;
|
||||
dtimNa: number;
|
||||
dtim6e: number;
|
||||
l2Isolation: boolean;
|
||||
fastRoamingEnabled: boolean;
|
||||
bssTransition: boolean;
|
||||
uapsdEnabled: boolean;
|
||||
iappEnabled: boolean;
|
||||
proxyArp: boolean;
|
||||
mcastenhanceEnabled: boolean;
|
||||
macFilterEnabled: boolean;
|
||||
macFilterPolicy: string;
|
||||
macFilterList: string[];
|
||||
radiusDasEnabled: boolean;
|
||||
radiusMacAuthEnabled: boolean;
|
||||
radiusMacaclFormat: string;
|
||||
minrateSettingPreference: string;
|
||||
minrateNgEnabled: boolean;
|
||||
minrateNgDataRateKbps: number;
|
||||
minrateNgAdvertisingRates: boolean;
|
||||
minrateNaEnabled: boolean;
|
||||
minrateNaDataRateKbps: number;
|
||||
minrateNaAdvertisingRates: boolean;
|
||||
settingPreference: string;
|
||||
no2ghzOui: boolean;
|
||||
privatePreSharedKeysEnabled: boolean;
|
||||
privatePreSharedKeys: unknown[];
|
||||
saeGroups: unknown[];
|
||||
saePsk: unknown[];
|
||||
schedule: unknown[];
|
||||
scheduleWithDuration: unknown[];
|
||||
bcFilterList: unknown[];
|
||||
externalId: string | null;
|
||||
}
|
||||
|
||||
export interface WlanConfUpdate {
|
||||
name?: string;
|
||||
x_passphrase?: string;
|
||||
enabled?: boolean;
|
||||
security?: "wpapsk" | "wpaeap" | "open" | "osen";
|
||||
wpa_mode?: "wpa2" | "wpa3" | "wpa2wpa3";
|
||||
wpa_enc?: "ccmp" | "gcmp" | "ccmp-gcmp";
|
||||
hide_ssid?: boolean;
|
||||
mac_filter_enabled?: boolean;
|
||||
mac_filter_policy?: "allow" | "deny";
|
||||
is_guest?: boolean;
|
||||
l2_isolation?: boolean;
|
||||
fast_roaming_enabled?: boolean;
|
||||
bss_transition?: boolean;
|
||||
uapsd_enabled?: boolean;
|
||||
group_rekey?: number;
|
||||
dtim_mode?: "default" | "custom";
|
||||
dtim_ng?: number;
|
||||
dtim_na?: number;
|
||||
minrate_ng_enabled?: boolean;
|
||||
minrate_na_enabled?: boolean;
|
||||
radius_das_enabled?: boolean;
|
||||
radius_mac_auth_enabled?: boolean;
|
||||
pmf_mode?: "disabled" | "optional" | "required";
|
||||
wlan_band?: "both" | "2g" | "5g";
|
||||
usergroup_id?: string;
|
||||
proxy_arp?: boolean;
|
||||
mcastenhance_enabled?: boolean;
|
||||
mac_filter_list?: string[];
|
||||
no2ghz_oui?: boolean;
|
||||
ap_group_ids?: string[];
|
||||
ap_group_mode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* CamelCase update input matching the WlanConf return shape.
|
||||
* Accepted by the API and converted to WlanConfUpdate (snake_case) before
|
||||
* being sent to the UniFi controller.
|
||||
*/
|
||||
export interface WlanConfUpdateInput {
|
||||
name?: string;
|
||||
passphrase?: string;
|
||||
enabled?: boolean;
|
||||
security?: "wpapsk" | "wpaeap" | "open" | "osen";
|
||||
wpaMode?: "wpa2" | "wpa3" | "wpa2wpa3";
|
||||
wpaEnc?: "ccmp" | "gcmp" | "ccmp-gcmp";
|
||||
hideSSID?: boolean;
|
||||
macFilterEnabled?: boolean;
|
||||
macFilterPolicy?: "allow" | "deny";
|
||||
isGuest?: boolean;
|
||||
l2Isolation?: boolean;
|
||||
fastRoamingEnabled?: boolean;
|
||||
bssTransition?: boolean;
|
||||
uapsdEnabled?: boolean;
|
||||
groupRekey?: number;
|
||||
dtimMode?: "default" | "custom";
|
||||
dtimNg?: number;
|
||||
dtimNa?: number;
|
||||
minrateNgEnabled?: boolean;
|
||||
minrateNaEnabled?: boolean;
|
||||
radiusDasEnabled?: boolean;
|
||||
radiusMacAuthEnabled?: boolean;
|
||||
pmfMode?: "disabled" | "optional" | "required";
|
||||
band?: "both" | "2g" | "5g";
|
||||
usergroupId?: string;
|
||||
proxyArp?: boolean;
|
||||
mcastenhanceEnabled?: boolean;
|
||||
macFilterList?: string[];
|
||||
no2ghzOui?: boolean;
|
||||
apGroupIds?: string[];
|
||||
apGroupMode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a camelCase WlanConfUpdateInput to the snake_case WlanConfUpdate
|
||||
* expected by the UniFi controller API.
|
||||
*/
|
||||
export function toWlanConfUpdate(input: WlanConfUpdateInput): WlanConfUpdate {
|
||||
const result: WlanConfUpdate = {};
|
||||
|
||||
if (input.name !== undefined) result.name = input.name;
|
||||
if (input.passphrase !== undefined) result.x_passphrase = input.passphrase;
|
||||
if (input.enabled !== undefined) result.enabled = input.enabled;
|
||||
if (input.security !== undefined) result.security = input.security;
|
||||
if (input.wpaMode !== undefined) result.wpa_mode = input.wpaMode;
|
||||
if (input.wpaEnc !== undefined) result.wpa_enc = input.wpaEnc;
|
||||
if (input.hideSSID !== undefined) result.hide_ssid = input.hideSSID;
|
||||
if (input.macFilterEnabled !== undefined)
|
||||
result.mac_filter_enabled = input.macFilterEnabled;
|
||||
if (input.macFilterPolicy !== undefined)
|
||||
result.mac_filter_policy = input.macFilterPolicy;
|
||||
if (input.isGuest !== undefined) result.is_guest = input.isGuest;
|
||||
if (input.l2Isolation !== undefined) result.l2_isolation = input.l2Isolation;
|
||||
if (input.fastRoamingEnabled !== undefined)
|
||||
result.fast_roaming_enabled = input.fastRoamingEnabled;
|
||||
if (input.bssTransition !== undefined)
|
||||
result.bss_transition = input.bssTransition;
|
||||
if (input.uapsdEnabled !== undefined)
|
||||
result.uapsd_enabled = input.uapsdEnabled;
|
||||
if (input.groupRekey !== undefined) result.group_rekey = input.groupRekey;
|
||||
if (input.dtimMode !== undefined) result.dtim_mode = input.dtimMode;
|
||||
if (input.dtimNg !== undefined) result.dtim_ng = input.dtimNg;
|
||||
if (input.dtimNa !== undefined) result.dtim_na = input.dtimNa;
|
||||
if (input.minrateNgEnabled !== undefined)
|
||||
result.minrate_ng_enabled = input.minrateNgEnabled;
|
||||
if (input.minrateNaEnabled !== undefined)
|
||||
result.minrate_na_enabled = input.minrateNaEnabled;
|
||||
if (input.radiusDasEnabled !== undefined)
|
||||
result.radius_das_enabled = input.radiusDasEnabled;
|
||||
if (input.radiusMacAuthEnabled !== undefined)
|
||||
result.radius_mac_auth_enabled = input.radiusMacAuthEnabled;
|
||||
if (input.pmfMode !== undefined) result.pmf_mode = input.pmfMode;
|
||||
if (input.band !== undefined) result.wlan_band = input.band;
|
||||
if (input.usergroupId !== undefined) result.usergroup_id = input.usergroupId;
|
||||
if (input.proxyArp !== undefined) result.proxy_arp = input.proxyArp;
|
||||
if (input.mcastenhanceEnabled !== undefined)
|
||||
result.mcastenhance_enabled = input.mcastenhanceEnabled;
|
||||
if (input.macFilterList !== undefined)
|
||||
result.mac_filter_list = input.macFilterList;
|
||||
if (input.no2ghzOui !== undefined) result.no2ghz_oui = input.no2ghzOui;
|
||||
if (input.apGroupIds !== undefined) result.ap_group_ids = input.apGroupIds;
|
||||
if (input.apGroupMode !== undefined) result.ap_group_mode = input.apGroupMode;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// --- Site overview types ---
|
||||
|
||||
export interface SubsystemHealth {
|
||||
subsystem: "wlan" | "wan" | "www" | "lan" | "vpn";
|
||||
status: "ok" | "warn" | "error" | "unknown";
|
||||
numUser?: number;
|
||||
numGuest?: number;
|
||||
numIot?: number;
|
||||
txBytesR?: number;
|
||||
rxBytesR?: number;
|
||||
// WLAN-specific
|
||||
numAp?: number;
|
||||
// LAN-specific
|
||||
numSw?: number;
|
||||
// WAN-specific
|
||||
numGw?: number;
|
||||
// Shared device counts
|
||||
numAdopted?: number;
|
||||
numDisconnected?: number;
|
||||
numPending?: number;
|
||||
numDisabled?: number;
|
||||
}
|
||||
|
||||
export interface SysInfo {
|
||||
name: string;
|
||||
hostname: string;
|
||||
version: string;
|
||||
build: string;
|
||||
timezone: string;
|
||||
uptime: number;
|
||||
ipAddresses: string[];
|
||||
updateAvailable: boolean;
|
||||
isCloudConsole: boolean;
|
||||
dataRetentionDays: number;
|
||||
informPort: number;
|
||||
httpsPort: number;
|
||||
unsupportedDeviceCount: number;
|
||||
}
|
||||
|
||||
export interface SiteInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
deviceCount: number;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface SiteOverview {
|
||||
site: SiteInfo;
|
||||
health: SubsystemHealth[];
|
||||
sysInfo: SysInfo;
|
||||
}
|
||||
|
||||
// --- Device types ---
|
||||
|
||||
export type DeviceType = "uap" | "usw" | "ugw" | "uxg" | "ubb" | "udm";
|
||||
export type DeviceState =
|
||||
| "connected"
|
||||
| "disconnected"
|
||||
| "pending"
|
||||
| "adopting"
|
||||
| "unknown";
|
||||
|
||||
export interface DeviceUplink {
|
||||
type?: string;
|
||||
mac?: string;
|
||||
ip?: string;
|
||||
uplinkRemotePort?: number;
|
||||
speed?: number;
|
||||
fullDuplex?: boolean;
|
||||
}
|
||||
|
||||
export interface DeviceRadio {
|
||||
name: string;
|
||||
radio: string;
|
||||
channel: number;
|
||||
txPower: number;
|
||||
txPowerMode: string;
|
||||
minRssiEnabled: boolean;
|
||||
numSta: number;
|
||||
satisfaction: number | null;
|
||||
}
|
||||
|
||||
export interface Device {
|
||||
id: string;
|
||||
mac: string;
|
||||
ip: string;
|
||||
name: string;
|
||||
model: string;
|
||||
shortname: string;
|
||||
type: DeviceType;
|
||||
version: string;
|
||||
serial: string;
|
||||
state: DeviceState;
|
||||
adopted: boolean;
|
||||
uptime: number;
|
||||
lastSeen: number;
|
||||
upgradable: boolean;
|
||||
satisfaction: number | null;
|
||||
numClients: number;
|
||||
numUserClients: number;
|
||||
numGuestClients: number;
|
||||
txBytes: number;
|
||||
rxBytes: number;
|
||||
uplink: DeviceUplink | null;
|
||||
radios: DeviceRadio[];
|
||||
modelInLts: boolean;
|
||||
modelInEol: boolean;
|
||||
}
|
||||
|
||||
// --- Network types ---
|
||||
|
||||
export type NetworkPurpose =
|
||||
| "corporate"
|
||||
| "vlan-only"
|
||||
| "wan"
|
||||
| "vpn-client"
|
||||
| "remote-user-vpn"
|
||||
| "site-vpn";
|
||||
|
||||
export interface Network {
|
||||
id: string;
|
||||
name: string;
|
||||
purpose: NetworkPurpose;
|
||||
enabled: boolean;
|
||||
ipSubnet: string | null;
|
||||
vlan: number | null;
|
||||
vlanEnabled: boolean;
|
||||
isNat: boolean;
|
||||
domainName: string | null;
|
||||
networkGroup: string | null;
|
||||
dhcpdEnabled: boolean;
|
||||
dhcpdStart: string | null;
|
||||
dhcpdStop: string | null;
|
||||
dhcpdLeasetime: number | null;
|
||||
dhcpRelayEnabled: boolean;
|
||||
dhcpGuardEnabled: boolean;
|
||||
igmpSnooping: boolean;
|
||||
ipv6Enabled: boolean;
|
||||
ipv6InterfaceType: string | null;
|
||||
internetAccessEnabled: boolean | null;
|
||||
}
|
||||
|
||||
// --- Site create types ---
|
||||
|
||||
export interface CreateSiteOptions {
|
||||
/** Human-readable description / display name for the site */
|
||||
description: string;
|
||||
}
|
||||
|
||||
// --- Site list types ---
|
||||
|
||||
export interface SiteListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
deviceCount: number;
|
||||
role: string;
|
||||
}
|
||||
|
||||
// --- WLAN Group types ---
|
||||
|
||||
export interface WlanGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
siteId: string;
|
||||
noDelete: boolean;
|
||||
noEdit: boolean;
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
export interface WlanGroupCreateInput {
|
||||
name: string;
|
||||
}
|
||||
|
||||
// --- AP Group types ---
|
||||
|
||||
export interface ApGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
deviceMacs: string[];
|
||||
noDelete: boolean;
|
||||
forWlanconf: boolean;
|
||||
}
|
||||
|
||||
// --- User Group (Speed Profile) types ---
|
||||
|
||||
export interface UserGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
siteId: string;
|
||||
noDelete: boolean;
|
||||
/** Download rate limit in Kbps. -1 means unlimited. */
|
||||
downloadLimitKbps: number;
|
||||
/** Upload rate limit in Kbps. -1 means unlimited. */
|
||||
uploadLimitKbps: number;
|
||||
}
|
||||
|
||||
export interface UserGroupCreateInput {
|
||||
name: string;
|
||||
/** Download rate limit in Kbps. -1 or omit for unlimited. */
|
||||
downloadLimitKbps?: number;
|
||||
/** Upload rate limit in Kbps. -1 or omit for unlimited. */
|
||||
uploadLimitKbps?: number;
|
||||
}
|
||||
|
||||
// --- Private PSK types ---
|
||||
|
||||
export interface PrivatePSK {
|
||||
key: string;
|
||||
name: string;
|
||||
mac: string | null;
|
||||
vlanId: number | null;
|
||||
}
|
||||
|
||||
export interface PrivatePSKCreateInput {
|
||||
key: string;
|
||||
name: string;
|
||||
mac?: string;
|
||||
vlanId?: number;
|
||||
}
|
||||
|
||||
// --- WiFi Limit types ---
|
||||
|
||||
export interface ApRadioWifiUsage {
|
||||
radio: string;
|
||||
band: string;
|
||||
activeWlans: number;
|
||||
limit: number;
|
||||
remaining: number;
|
||||
wlanNames: string[];
|
||||
}
|
||||
|
||||
export interface ApWifiLimits {
|
||||
apId: string;
|
||||
apName: string;
|
||||
mac: string;
|
||||
model: string;
|
||||
radios: ApRadioWifiUsage[];
|
||||
}
|
||||
Reference in New Issue
Block a user