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:
21
changelog.md
Normal file
21
changelog.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-02 - 1.1.0 - feat(unifi)
|
||||||
|
implement comprehensive UniFi API client with controllers, protect, access, account, managers, resources, HTTP client, interfaces, logging, plugins, and tests
|
||||||
|
|
||||||
|
- Add full TypeScript implementation for UniFi APIs: UnifiController, UnifiProtect, UnifiAccess, UnifiAccount entry points
|
||||||
|
- Introduce manager classes: DeviceManager, ClientManager, CameraManager, DoorManager, SiteManager, HostManager
|
||||||
|
- Add resource classes: UnifiDevice, UnifiClient, UnifiCamera, UnifiDoor, UnifiSite, UnifiHost
|
||||||
|
- Implement UnifiHttp HTTP client with SSL/cookie handling and raw response support
|
||||||
|
- Add comprehensive interface definitions for network, protect, access, sitemanager and common types
|
||||||
|
- Update plugins to re-export smartlog, smartpromise, smartrequest, smartstring and provide SmartRequestClient types
|
||||||
|
- Add unifi.logger configured with smartlog
|
||||||
|
- Add many unit and integration tests under test/ (network, protect, site, info) and expand test/test.ts
|
||||||
|
- Expand README and add readme.hints.md with architecture and usage notes
|
||||||
|
- Update package.json: add new @push.rocks dependencies and @push.rocks/qenv devDependency; bump package metadata
|
||||||
|
- Update npmextra.json to include custom registries
|
||||||
|
|
||||||
|
## 2026-02-02 - 1.0.1 - initial release
|
||||||
|
Initial commit: project repository created.
|
||||||
|
|
||||||
|
- Added initial project files and baseline repository state.
|
||||||
@@ -11,7 +11,11 @@
|
|||||||
"projectDomain": "apiclient.xyz"
|
"projectDomain": "apiclient.xyz"
|
||||||
},
|
},
|
||||||
"release": {
|
"release": {
|
||||||
"accessLevel": "public"
|
"accessLevel": "public",
|
||||||
|
"registries": [
|
||||||
|
"https://verdaccio.lossless.digital",
|
||||||
|
"https://registry.npmjs.org"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@ship.zone/szci": {
|
"@ship.zone/szci": {
|
||||||
|
|||||||
@@ -17,10 +17,15 @@
|
|||||||
"@git.zone/tsbuild": "^4.1.2",
|
"@git.zone/tsbuild": "^4.1.2",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@git.zone/tstest": "^3.1.8",
|
"@git.zone/tstest": "^3.1.8",
|
||||||
|
"@push.rocks/qenv": "^6.1.0",
|
||||||
"@types/node": "^25.2.0"
|
"@types/node": "^25.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/smartpath": "^6.0.0"
|
"@push.rocks/smartlog": "^3.0.0",
|
||||||
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
|
"@push.rocks/smartrequest": "^2.1.0",
|
||||||
|
"@push.rocks/smartstring": "^4.0.15"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -8,9 +8,21 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@push.rocks/smartlog':
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.1.10
|
||||||
'@push.rocks/smartpath':
|
'@push.rocks/smartpath':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
|
'@push.rocks/smartpromise':
|
||||||
|
specifier: ^4.2.3
|
||||||
|
version: 4.2.3
|
||||||
|
'@push.rocks/smartrequest':
|
||||||
|
specifier: ^2.1.0
|
||||||
|
version: 2.1.0
|
||||||
|
'@push.rocks/smartstring':
|
||||||
|
specifier: ^4.0.15
|
||||||
|
version: 4.1.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@git.zone/tsbuild':
|
'@git.zone/tsbuild':
|
||||||
specifier: ^4.1.2
|
specifier: ^4.1.2
|
||||||
@@ -21,6 +33,9 @@ importers:
|
|||||||
'@git.zone/tstest':
|
'@git.zone/tstest':
|
||||||
specifier: ^3.1.8
|
specifier: ^3.1.8
|
||||||
version: 3.1.8(socks@2.8.7)(typescript@5.9.3)
|
version: 3.1.8(socks@2.8.7)(typescript@5.9.3)
|
||||||
|
'@push.rocks/qenv':
|
||||||
|
specifier: ^6.1.0
|
||||||
|
version: 6.1.3
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.2.0
|
specifier: ^25.2.0
|
||||||
version: 25.2.0
|
version: 25.2.0
|
||||||
|
|||||||
@@ -1,3 +1,74 @@
|
|||||||
# Project Readme Hints
|
# Project Readme Hints
|
||||||
|
|
||||||
This is the initial readme hints file.
|
## Architecture Overview
|
||||||
|
|
||||||
|
This package implements a comprehensive UniFi API client supporting four different UniFi applications:
|
||||||
|
|
||||||
|
### Entry Points
|
||||||
|
|
||||||
|
1. **UnifiAccount** - Site Manager (cloud) API
|
||||||
|
- Uses API key authentication via `X-API-Key` header
|
||||||
|
- Base URL: `https://api.ui.com/v1`
|
||||||
|
- Managers: SiteManager, HostManager
|
||||||
|
|
||||||
|
2. **UnifiController** - Network Controller API (local)
|
||||||
|
- Uses session cookie authentication
|
||||||
|
- Supports multiple controller types: `unifi-os`, `udm-pro`, `standalone`
|
||||||
|
- Managers: DeviceManager, ClientManager
|
||||||
|
|
||||||
|
3. **UnifiProtect** - Protect NVR API (local)
|
||||||
|
- Uses session cookie + CSRF token authentication
|
||||||
|
- Fetches bootstrap on login for camera/NVR info
|
||||||
|
- Manager: CameraManager
|
||||||
|
|
||||||
|
4. **UnifiAccess** - Access Controller API (local)
|
||||||
|
- Uses bearer token authentication
|
||||||
|
- Default port: 12445
|
||||||
|
- Manager: DoorManager
|
||||||
|
|
||||||
|
### Key Files
|
||||||
|
|
||||||
|
- `ts/classes.unifihttp.ts` - Base HTTP client with SSL handling
|
||||||
|
- `ts/interfaces/*.ts` - All TypeScript interfaces
|
||||||
|
- `ts/unifi.logger.ts` - Logging utility
|
||||||
|
|
||||||
|
### Authentication Notes
|
||||||
|
|
||||||
|
- **UniFi OS consoles** (UDM, UDM Pro, Cloud Key Gen2+) authenticate at console level (`/api/auth/login`) then access Network API via `/proxy/network`
|
||||||
|
- **Standalone controllers** authenticate directly at `/api/login`
|
||||||
|
- **Protect API** is behind `/proxy/protect/api`
|
||||||
|
- **Access API** uses port 12445 with bearer token
|
||||||
|
|
||||||
|
### SSL/TLS
|
||||||
|
|
||||||
|
All local APIs default to `verifySsl: false` to support self-signed certificates common on UniFi devices.
|
||||||
|
|
||||||
|
### Testing Environment Variables
|
||||||
|
|
||||||
|
Required in `.nogit/env.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"UNIFI_CONSOLE_IP": "192.168.x.x",
|
||||||
|
"UNIFI_NETWORK_DEV_KEY": "xxx",
|
||||||
|
"UNIFI_PROTECT_DEV_KEY": "xxx"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Files
|
||||||
|
|
||||||
|
- `test/test.ts` - Unit tests (no credentials needed)
|
||||||
|
- `test/test.network.ts` - Network Controller integration tests
|
||||||
|
- `test/test.protect.ts` - Protect API integration tests (defensive - handles missing NVR/camera data gracefully)
|
||||||
|
|
||||||
|
### API Notes
|
||||||
|
|
||||||
|
- **Network API** works perfectly with local console API keys
|
||||||
|
- **Protect API** bootstrap structure may vary - NVR info may not be available with API key auth
|
||||||
|
- **Events endpoint** may return array or `{data: [...]}` format - CameraManager handles both
|
||||||
|
- **Site Manager cloud API** (`api.ui.com`) requires different keys (from ui.com portal), not local console keys
|
||||||
|
|
||||||
|
### Documentation Sources
|
||||||
|
|
||||||
|
- Local docs in `.nogit/docs/` contain OpenAPI specs from console
|
||||||
|
- Network API: `/unifi-api/network` on console
|
||||||
|
- Protect API: `/unifi-api/protect` on console
|
||||||
|
|||||||
481
readme.md
481
readme.md
@@ -1,7 +1,482 @@
|
|||||||
# @apiclient.xyz/unifi
|
# @apiclient.xyz/unifi
|
||||||
|
|
||||||
an unofficial unifi api package
|
A comprehensive, unofficial TypeScript client for the UniFi ecosystem. Control your entire Ubiquiti infrastructure programmatically — Network devices, Protect cameras, Access doors, and Site Manager — all from a single, unified API. 🚀
|
||||||
|
|
||||||
## How to create the docs
|
## Issue Reporting and Security
|
||||||
|
|
||||||
To create docs run gitzone aidoc.
|
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @apiclient.xyz/unifi
|
||||||
|
# or
|
||||||
|
pnpm add @apiclient.xyz/unifi
|
||||||
|
# or
|
||||||
|
yarn add @apiclient.xyz/unifi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
✨ **Four UniFi APIs in One Package**
|
||||||
|
|
||||||
|
| API | Use Case | Authentication |
|
||||||
|
|-----|----------|----------------|
|
||||||
|
| **UnifiController** | Network devices, clients, VLANs, WLANs | API Key or Session |
|
||||||
|
| **UnifiProtect** | Cameras, NVR, motion events, recordings | API Key or Session + CSRF |
|
||||||
|
| **UnifiAccess** | Doors, users, NFC cards, access events | Bearer Token |
|
||||||
|
| **UnifiAccount** | Cloud Site Manager (ui.com) | API Key |
|
||||||
|
|
||||||
|
🎯 **Developer-Friendly Design**
|
||||||
|
- Full TypeScript support with comprehensive types
|
||||||
|
- Intuitive class-based resource management
|
||||||
|
- Async/await throughout
|
||||||
|
- Works with self-signed certificates (common on UniFi devices)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Network Controller (Local)
|
||||||
|
|
||||||
|
Manage your switches, access points, gateways, and connected clients:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { UnifiController } from '@apiclient.xyz/unifi';
|
||||||
|
|
||||||
|
// Using API key (recommended - no login required)
|
||||||
|
const controller = new UnifiController({
|
||||||
|
host: '192.168.1.1',
|
||||||
|
apiKey: 'your-network-api-key',
|
||||||
|
controllerType: 'unifi-os', // 'unifi-os' | 'udm-pro' | 'standalone'
|
||||||
|
verifySsl: false, // Self-signed certs
|
||||||
|
});
|
||||||
|
|
||||||
|
// List all devices
|
||||||
|
const devices = await controller.deviceManager.listDevices();
|
||||||
|
for (const device of devices) {
|
||||||
|
console.log(`${device.getDisplayName()} - ${device.isOnline() ? '🟢' : '🔴'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get specific device types
|
||||||
|
const accessPoints = await controller.deviceManager.getAccessPoints();
|
||||||
|
const switches = await controller.deviceManager.getSwitches();
|
||||||
|
const gateways = await controller.deviceManager.getGateways();
|
||||||
|
|
||||||
|
// Manage connected clients
|
||||||
|
const clients = await controller.clientManager.listActiveClients();
|
||||||
|
const wirelessClients = await controller.clientManager.getWirelessClients();
|
||||||
|
|
||||||
|
// Find a client and block them
|
||||||
|
const troublemaker = await controller.clientManager.getClientByMac('aa:bb:cc:dd:ee:ff');
|
||||||
|
if (troublemaker) {
|
||||||
|
await troublemaker.block();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get network configuration
|
||||||
|
const networks = await controller.getNetworks();
|
||||||
|
const wlans = await controller.getWlans();
|
||||||
|
const firewallRules = await controller.getFirewallRules();
|
||||||
|
const portForwards = await controller.getPortForwards();
|
||||||
|
|
||||||
|
// System info and health
|
||||||
|
const health = await controller.getHealth();
|
||||||
|
const systemInfo = await controller.getSystemInfo();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protect NVR (Local)
|
||||||
|
|
||||||
|
Control your cameras, view motion events, manage recordings:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { UnifiProtect } from '@apiclient.xyz/unifi';
|
||||||
|
|
||||||
|
const protect = new UnifiProtect({
|
||||||
|
host: '192.168.1.1',
|
||||||
|
apiKey: 'your-protect-api-key',
|
||||||
|
verifySsl: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load camera data
|
||||||
|
await protect.refreshBootstrap();
|
||||||
|
|
||||||
|
// List all cameras
|
||||||
|
const cameras = await protect.cameraManager.listCameras();
|
||||||
|
for (const camera of cameras) {
|
||||||
|
console.log(`📷 ${camera.name} - ${camera.isOnline() ? 'Online' : 'Offline'}`);
|
||||||
|
if (camera.isDoorbell()) console.log(' 🔔 Doorbell');
|
||||||
|
if (camera.hasSmartDetect()) console.log(' 🧠 Smart Detection enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cameras by status
|
||||||
|
const onlineCameras = await protect.cameraManager.getOnlineCameras();
|
||||||
|
const doorbells = await protect.cameraManager.getDoorbells();
|
||||||
|
const smartCameras = await protect.cameraManager.getSmartDetectCameras();
|
||||||
|
|
||||||
|
// Check recent motion
|
||||||
|
const recentMotion = await protect.cameraManager.getCamerasWithRecentMotion(300); // Last 5 min
|
||||||
|
const motionEvents = await protect.cameraManager.getAllMotionEvents({ limit: 50 });
|
||||||
|
|
||||||
|
// Control a camera
|
||||||
|
const frontDoor = await protect.cameraManager.getCameraByName('Front Door');
|
||||||
|
if (frontDoor) {
|
||||||
|
await frontDoor.setRecordingMode('always'); // 'always' | 'detections' | 'never' | 'schedule'
|
||||||
|
await frontDoor.setMicVolume(80);
|
||||||
|
await frontDoor.restart();
|
||||||
|
|
||||||
|
// Get RTSP stream URL
|
||||||
|
const rtspUrl = frontDoor.getRtspUrl(0); // Channel 0 = highest quality
|
||||||
|
}
|
||||||
|
|
||||||
|
// NVR info
|
||||||
|
const nvrInfo = protect.getNvrInfo();
|
||||||
|
const storageInfo = protect.getStorageInfo();
|
||||||
|
const lights = protect.getLights();
|
||||||
|
const sensors = protect.getSensors();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access Controller (Local)
|
||||||
|
|
||||||
|
Manage doors, users, credentials, and access events:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { UnifiAccess } from '@apiclient.xyz/unifi';
|
||||||
|
|
||||||
|
const access = new UnifiAccess({
|
||||||
|
host: '192.168.1.1',
|
||||||
|
token: 'your-bearer-token',
|
||||||
|
verifySsl: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// List all doors
|
||||||
|
const doors = await access.doorManager.listDoors();
|
||||||
|
for (const door of doors) {
|
||||||
|
console.log(`🚪 ${door.getDisplayName()} - ${door.getStatus()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control a door
|
||||||
|
const mainEntrance = await access.doorManager.getDoorByName('Main Entrance');
|
||||||
|
if (mainEntrance) {
|
||||||
|
await mainEntrance.unlock();
|
||||||
|
// Door auto-locks based on your Access settings
|
||||||
|
|
||||||
|
console.log(`Locked: ${mainEntrance.isLocked()}`);
|
||||||
|
console.log(`Open: ${mainEntrance.isOpen()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage users
|
||||||
|
const users = await access.getUsers();
|
||||||
|
const newUser = await access.createUser({
|
||||||
|
first_name: 'John',
|
||||||
|
last_name: 'Doe',
|
||||||
|
email: 'john@example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign credentials
|
||||||
|
await access.setUserPin(newUser.id, '1234');
|
||||||
|
await access.assignNfcCard(newUser.id, 'card-token-here', 'Office Card');
|
||||||
|
|
||||||
|
// Grant/revoke door access
|
||||||
|
await access.grantAccess(newUser.id, mainEntrance.unique_id);
|
||||||
|
await access.revokeAccess(newUser.id, mainEntrance.unique_id);
|
||||||
|
|
||||||
|
// View access events
|
||||||
|
const recentEvents = await access.getRecentEvents(100);
|
||||||
|
const doorEvents = await access.getEvents({ doorId: mainEntrance.unique_id });
|
||||||
|
|
||||||
|
// Get policies and locations
|
||||||
|
const policies = await access.getPolicies();
|
||||||
|
const locations = await access.getLocations();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Site Manager (Cloud)
|
||||||
|
|
||||||
|
Manage multiple sites via the ui.com cloud API:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { UnifiAccount } from '@apiclient.xyz/unifi';
|
||||||
|
|
||||||
|
// Cloud API uses api.ui.com
|
||||||
|
const account = new UnifiAccount({
|
||||||
|
apiKey: 'your-cloud-api-key', // From ui.com
|
||||||
|
});
|
||||||
|
|
||||||
|
// List all sites
|
||||||
|
const sites = await account.siteManager.listSites();
|
||||||
|
for (const site of sites) {
|
||||||
|
console.log(`🏢 ${site.name} (${site.siteId})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all hosts (consoles)
|
||||||
|
const hosts = await account.hostManager.listHosts();
|
||||||
|
|
||||||
|
// Find specific site
|
||||||
|
const mainOffice = await account.siteManager.findSiteByName('Main Office');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication Methods
|
||||||
|
|
||||||
|
### API Keys (Recommended)
|
||||||
|
|
||||||
|
Generate API keys in your UniFi console settings. API keys don't expire and don't require login/logout:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Network API key
|
||||||
|
const controller = new UnifiController({
|
||||||
|
host: '192.168.1.1',
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
controllerType: 'unifi-os',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Already authenticated - start using immediately
|
||||||
|
const devices = await controller.deviceManager.listDevices();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Authentication
|
||||||
|
|
||||||
|
For scenarios where API keys aren't available:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const controller = new UnifiController({
|
||||||
|
host: '192.168.1.1',
|
||||||
|
username: 'admin',
|
||||||
|
password: 'your-password',
|
||||||
|
controllerType: 'unifi-os',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Must login first
|
||||||
|
await controller.login();
|
||||||
|
|
||||||
|
// Use the API
|
||||||
|
const devices = await controller.deviceManager.listDevices();
|
||||||
|
|
||||||
|
// Logout when done
|
||||||
|
await controller.logout();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Device Management
|
||||||
|
|
||||||
|
### Working with UnifiDevice
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const device = await controller.deviceManager.getDeviceByMac('aa:bb:cc:dd:ee:ff');
|
||||||
|
|
||||||
|
// Status checks
|
||||||
|
device.isOnline(); // Connected to controller?
|
||||||
|
device.isAccessPoint(); // Is this an AP?
|
||||||
|
device.isSwitch(); // Is this a switch?
|
||||||
|
device.isGateway(); // Is this a router/gateway?
|
||||||
|
device.hasUpgrade(); // Firmware update available?
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
await device.restart();
|
||||||
|
await device.upgrade();
|
||||||
|
await device.rename('New Device Name');
|
||||||
|
await device.setLedOverride('off'); // 'default' | 'on' | 'off'
|
||||||
|
|
||||||
|
// Switch port configuration
|
||||||
|
await device.setPortConfig(1, {
|
||||||
|
poe_mode: 'auto',
|
||||||
|
name: 'Camera Port',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
device.ip; // IP address
|
||||||
|
device.mac; // MAC address
|
||||||
|
device.model; // Model code (e.g., 'USW-24-POE')
|
||||||
|
device.version; // Firmware version
|
||||||
|
device.uptime; // Uptime in seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
### Working with UnifiClient
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const client = await controller.clientManager.getClientByIp('192.168.1.100');
|
||||||
|
|
||||||
|
// Connection info
|
||||||
|
client.isWireless(); // WiFi or wired?
|
||||||
|
client.isGuest(); // Guest network?
|
||||||
|
client.getConnectionType(); // "Wireless (MySSID)" or "Wired (Port 5)"
|
||||||
|
client.getSignalQuality(); // "Excellent" | "Good" | "Fair" | "Poor"
|
||||||
|
client.getDataUsage(); // Total bytes (TX + RX)
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
await client.block(); // Block from network
|
||||||
|
await client.unblock(); // Unblock
|
||||||
|
await client.reconnect(); // Kick and reconnect
|
||||||
|
await client.rename('Living Room TV');
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
client.ip; // IP address
|
||||||
|
client.mac; // MAC address
|
||||||
|
client.hostname; // Device hostname
|
||||||
|
client.essid; // WiFi network name
|
||||||
|
client.signal; // Signal strength (dBm)
|
||||||
|
client.tx_bytes; // Upload bytes
|
||||||
|
client.rx_bytes; // Download bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Working with UnifiCamera
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const camera = await protect.cameraManager.getCameraById('camera-id');
|
||||||
|
|
||||||
|
// Status
|
||||||
|
camera.isOnline();
|
||||||
|
camera.isDoorbell();
|
||||||
|
camera.hasSmartDetect();
|
||||||
|
camera.hasRecentMotion(60); // Motion in last 60 seconds?
|
||||||
|
camera.getTimeSinceLastMotion(); // Seconds since last motion
|
||||||
|
|
||||||
|
// Streaming
|
||||||
|
camera.getRtspUrl(0); // High quality RTSP
|
||||||
|
camera.getHighQualityChannel();
|
||||||
|
camera.getMediumQualityChannel();
|
||||||
|
camera.getLowQualityChannel();
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
await camera.setRecordingMode('detections');
|
||||||
|
await camera.setSmartDetectTypes(['person', 'vehicle']);
|
||||||
|
await camera.setMicVolume(50);
|
||||||
|
await camera.setSpeakerVolume(75);
|
||||||
|
await camera.rename('Garage Camera');
|
||||||
|
await camera.restart();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Working with UnifiDoor
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const door = await access.doorManager.getDoorById('door-id');
|
||||||
|
|
||||||
|
// Status
|
||||||
|
door.isLocked(); // Lock engaged?
|
||||||
|
door.isOpen(); // Door physically open?
|
||||||
|
door.isClosed(); // Door physically closed?
|
||||||
|
door.getStatus(); // "Locked, Closed" etc.
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
await door.unlock();
|
||||||
|
await door.lock();
|
||||||
|
await door.rename('Back Door');
|
||||||
|
await door.setAlias('Employee Entrance');
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Entry Point Classes
|
||||||
|
|
||||||
|
| Class | Description | Manager Classes |
|
||||||
|
|-------|-------------|-----------------|
|
||||||
|
| `UnifiController` | Local Network Controller | `deviceManager`, `clientManager` |
|
||||||
|
| `UnifiProtect` | Local Protect NVR | `cameraManager` |
|
||||||
|
| `UnifiAccess` | Local Access Controller | `doorManager` |
|
||||||
|
| `UnifiAccount` | Cloud Site Manager | `siteManager`, `hostManager` |
|
||||||
|
|
||||||
|
### Resource Classes
|
||||||
|
|
||||||
|
| Class | Represents |
|
||||||
|
|-------|------------|
|
||||||
|
| `UnifiDevice` | Network device (AP, switch, gateway) |
|
||||||
|
| `UnifiClient` | Connected network client |
|
||||||
|
| `UnifiCamera` | Protect camera |
|
||||||
|
| `UnifiDoor` | Access door |
|
||||||
|
| `UnifiSite` | Site Manager site |
|
||||||
|
| `UnifiHost` | Site Manager host/console |
|
||||||
|
|
||||||
|
### Controller Types
|
||||||
|
|
||||||
|
| Type | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `unifi-os` | UniFi OS consoles (UDM, UDM Pro, Cloud Key Gen2+) |
|
||||||
|
| `udm-pro` | Alias for unifi-os |
|
||||||
|
| `standalone` | Standalone software controller |
|
||||||
|
|
||||||
|
## SSL/TLS Handling
|
||||||
|
|
||||||
|
UniFi devices typically use self-signed certificates. Set `verifySsl: false` to allow connections:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const controller = new UnifiController({
|
||||||
|
host: '192.168.1.1',
|
||||||
|
apiKey: 'key',
|
||||||
|
verifySsl: false, // Allow self-signed certs
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
For production environments with proper certificates, set `verifySsl: true`.
|
||||||
|
|
||||||
|
## Environment Variables Example
|
||||||
|
|
||||||
|
Create a `.env` or use your preferred env management:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Network Controller
|
||||||
|
UNIFI_CONSOLE_IP=192.168.1.1
|
||||||
|
UNIFI_NETWORK_API_KEY=your-network-key
|
||||||
|
|
||||||
|
# Protect
|
||||||
|
UNIFI_PROTECT_API_KEY=your-protect-key
|
||||||
|
|
||||||
|
# Access
|
||||||
|
UNIFI_ACCESS_HOST=192.168.1.1
|
||||||
|
UNIFI_ACCESS_TOKEN=your-bearer-token
|
||||||
|
|
||||||
|
# Site Manager (Cloud)
|
||||||
|
UNIFI_CLOUD_API_KEY=your-cloud-key
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript Support
|
||||||
|
|
||||||
|
This package is written in TypeScript and exports comprehensive types:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
// Entry points
|
||||||
|
UnifiController,
|
||||||
|
UnifiProtect,
|
||||||
|
UnifiAccess,
|
||||||
|
UnifiAccount,
|
||||||
|
|
||||||
|
// Resources
|
||||||
|
UnifiDevice,
|
||||||
|
UnifiClient,
|
||||||
|
UnifiCamera,
|
||||||
|
UnifiDoor,
|
||||||
|
UnifiSite,
|
||||||
|
UnifiHost,
|
||||||
|
|
||||||
|
// Managers
|
||||||
|
DeviceManager,
|
||||||
|
ClientManager,
|
||||||
|
CameraManager,
|
||||||
|
DoorManager,
|
||||||
|
SiteManager,
|
||||||
|
HostManager,
|
||||||
|
|
||||||
|
// Interfaces
|
||||||
|
INetworkDevice,
|
||||||
|
INetworkClient,
|
||||||
|
IProtectCamera,
|
||||||
|
IAccessDoor,
|
||||||
|
// ... and many more
|
||||||
|
} from '@apiclient.xyz/unifi';
|
||||||
|
```
|
||||||
|
|
||||||
|
## License and Legal Information
|
||||||
|
|
||||||
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||||
|
|
||||||
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
### Trademarks
|
||||||
|
|
||||||
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||||
|
|
||||||
|
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||||
|
|
||||||
|
### Company Information
|
||||||
|
|
||||||
|
Task Venture Capital GmbH
|
||||||
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
|
|||||||
328
test/test.info.ts
Normal file
328
test/test.info.ts
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
|
import * as unifi from '../ts/index.js';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SITE INFO - Pretty print comprehensive site information
|
||||||
|
// Tests may use live production keys to test specific features at scale.
|
||||||
|
// Make sure to avoid dangerous, destructive or security relevant operations.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const testQenv = new Qenv('./', './.nogit/');
|
||||||
|
|
||||||
|
let testController: unifi.UnifiController;
|
||||||
|
|
||||||
|
// Helper to print section headers
|
||||||
|
const printHeader = (title: string) => {
|
||||||
|
console.log('');
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
console.log(` ${title}`);
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to print sub-section headers
|
||||||
|
const printSubHeader = (title: string) => {
|
||||||
|
console.log('');
|
||||||
|
console.log(`--- ${title} ---`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to format bytes
|
||||||
|
const formatBytes = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
tap.test('setup - create UnifiController', async () => {
|
||||||
|
const host = await testQenv.getEnvVarOnDemand('UNIFI_CONSOLE_IP');
|
||||||
|
const apiKey = await testQenv.getEnvVarOnDemand('UNIFI_NETWORK_DEV_KEY');
|
||||||
|
|
||||||
|
testController = new unifi.UnifiController({
|
||||||
|
host,
|
||||||
|
apiKey,
|
||||||
|
controllerType: 'unifi-os',
|
||||||
|
verifySsl: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(testController.isAuthenticated()).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('INFO - Sites Overview', async () => {
|
||||||
|
printHeader('SITES OVERVIEW');
|
||||||
|
|
||||||
|
const sites = await testController.listSites();
|
||||||
|
console.log(`Total Sites: ${sites.length}`);
|
||||||
|
|
||||||
|
for (const site of sites) {
|
||||||
|
console.log('');
|
||||||
|
console.log(` Site: ${site.name || site.desc || 'Unnamed'}`);
|
||||||
|
console.log(` ID: ${site._id}`);
|
||||||
|
console.log(` Description: ${site.desc || 'N/A'}`);
|
||||||
|
if (site.role) console.log(` Role: ${site.role}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('INFO - Devices by Type', async () => {
|
||||||
|
printHeader('DEVICES');
|
||||||
|
|
||||||
|
const devices = await testController.deviceManager.listDevices();
|
||||||
|
const accessPoints = await testController.deviceManager.getAccessPoints();
|
||||||
|
const switches = await testController.deviceManager.getSwitches();
|
||||||
|
const gateways = await testController.deviceManager.getGateways();
|
||||||
|
|
||||||
|
console.log(`Total Devices: ${devices.length}`);
|
||||||
|
console.log(` - Access Points: ${accessPoints.length}`);
|
||||||
|
console.log(` - Switches: ${switches.length}`);
|
||||||
|
console.log(` - Gateways: ${gateways.length}`);
|
||||||
|
|
||||||
|
// Access Points
|
||||||
|
if (accessPoints.length > 0) {
|
||||||
|
printSubHeader('Access Points');
|
||||||
|
for (const ap of accessPoints) {
|
||||||
|
const status = ap.isOnline() ? 'ONLINE' : 'OFFLINE';
|
||||||
|
console.log(` [${status}] ${ap.getDisplayName()}`);
|
||||||
|
console.log(` Model: ${ap.model} | IP: ${ap.ip || 'N/A'}`);
|
||||||
|
console.log(` MAC: ${ap.mac} | Version: ${ap.version || 'N/A'}`);
|
||||||
|
if (ap.uptime) {
|
||||||
|
const uptimeHours = Math.floor(ap.uptime / 3600);
|
||||||
|
const uptimeDays = Math.floor(uptimeHours / 24);
|
||||||
|
console.log(` Uptime: ${uptimeDays}d ${uptimeHours % 24}h`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switches
|
||||||
|
if (switches.length > 0) {
|
||||||
|
printSubHeader('Switches');
|
||||||
|
for (const sw of switches) {
|
||||||
|
const status = sw.isOnline() ? 'ONLINE' : 'OFFLINE';
|
||||||
|
console.log(` [${status}] ${sw.getDisplayName()}`);
|
||||||
|
console.log(` Model: ${sw.model} | IP: ${sw.ip || 'N/A'}`);
|
||||||
|
console.log(` MAC: ${sw.mac} | Ports: ${sw.port_table?.length || 'N/A'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gateways
|
||||||
|
if (gateways.length > 0) {
|
||||||
|
printSubHeader('Gateways');
|
||||||
|
for (const gw of gateways) {
|
||||||
|
const status = gw.isOnline() ? 'ONLINE' : 'OFFLINE';
|
||||||
|
console.log(` [${status}] ${gw.getDisplayName()}`);
|
||||||
|
console.log(` Model: ${gw.model} | IP: ${gw.ip || 'N/A'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('INFO - Networks (VLANs)', async () => {
|
||||||
|
printHeader('NETWORKS');
|
||||||
|
|
||||||
|
const response = await testController.getNetworks() as any;
|
||||||
|
const networks = response?.data || [];
|
||||||
|
|
||||||
|
console.log(`Total Networks: ${networks.length}`);
|
||||||
|
|
||||||
|
for (const net of networks) {
|
||||||
|
console.log('');
|
||||||
|
console.log(` Network: ${net.name}`);
|
||||||
|
console.log(` Purpose: ${net.purpose || 'N/A'}`);
|
||||||
|
if (net.vlan_enabled) {
|
||||||
|
console.log(` VLAN ID: ${net.vlan}`);
|
||||||
|
}
|
||||||
|
if (net.ip_subnet) {
|
||||||
|
console.log(` Subnet: ${net.ip_subnet}`);
|
||||||
|
}
|
||||||
|
if (net.dhcpd_enabled !== undefined) {
|
||||||
|
console.log(` DHCP: ${net.dhcpd_enabled ? 'Enabled' : 'Disabled'}`);
|
||||||
|
if (net.dhcpd_enabled && net.dhcpd_start && net.dhcpd_stop) {
|
||||||
|
console.log(` DHCP Range: ${net.dhcpd_start} - ${net.dhcpd_stop}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (net.igmp_snooping !== undefined) {
|
||||||
|
console.log(` IGMP Snooping: ${net.igmp_snooping ? 'Enabled' : 'Disabled'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('INFO - Wireless Networks (WLANs)', async () => {
|
||||||
|
printHeader('WIRELESS NETWORKS');
|
||||||
|
|
||||||
|
const response = await testController.getWlans() as any;
|
||||||
|
const wlans = response?.data || [];
|
||||||
|
|
||||||
|
console.log(`Total WLANs: ${wlans.length}`);
|
||||||
|
|
||||||
|
for (const wlan of wlans) {
|
||||||
|
console.log('');
|
||||||
|
console.log(` SSID: ${wlan.name}`);
|
||||||
|
console.log(` Enabled: ${wlan.enabled !== false ? 'Yes' : 'No'}`);
|
||||||
|
console.log(` Security: ${wlan.security || 'open'}`);
|
||||||
|
if (wlan.wpa_mode) {
|
||||||
|
console.log(` WPA Mode: ${wlan.wpa_mode}`);
|
||||||
|
}
|
||||||
|
if (wlan.networkconf_id) {
|
||||||
|
console.log(` Network ID: ${wlan.networkconf_id}`);
|
||||||
|
}
|
||||||
|
if (wlan.is_guest !== undefined) {
|
||||||
|
console.log(` Guest Network: ${wlan.is_guest ? 'Yes' : 'No'}`);
|
||||||
|
}
|
||||||
|
if (wlan.hide_ssid !== undefined) {
|
||||||
|
console.log(` Hidden: ${wlan.hide_ssid ? 'Yes' : 'No'}`);
|
||||||
|
}
|
||||||
|
if (wlan.wlan_band) {
|
||||||
|
console.log(` Band: ${wlan.wlan_band}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('INFO - Firewall Rules', async () => {
|
||||||
|
printHeader('FIREWALL RULES');
|
||||||
|
|
||||||
|
const response = await testController.getFirewallRules() as any;
|
||||||
|
const rules = response?.data || [];
|
||||||
|
|
||||||
|
console.log(`Total Firewall Rules: ${rules.length}`);
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
const enabled = rule.enabled !== false ? 'ON' : 'OFF';
|
||||||
|
console.log('');
|
||||||
|
console.log(` [${enabled}] ${rule.name || 'Unnamed Rule'}`);
|
||||||
|
console.log(` Action: ${rule.action || 'N/A'} | Ruleset: ${rule.ruleset || 'N/A'}`);
|
||||||
|
if (rule.src_firewallgroup_ids?.length > 0) {
|
||||||
|
console.log(` Source Groups: ${rule.src_firewallgroup_ids.length}`);
|
||||||
|
}
|
||||||
|
if (rule.dst_firewallgroup_ids?.length > 0) {
|
||||||
|
console.log(` Dest Groups: ${rule.dst_firewallgroup_ids.length}`);
|
||||||
|
}
|
||||||
|
if (rule.protocol) {
|
||||||
|
console.log(` Protocol: ${rule.protocol}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules.length === 0) {
|
||||||
|
console.log(' No custom firewall rules configured');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('INFO - Port Forwards', async () => {
|
||||||
|
printHeader('PORT FORWARDS');
|
||||||
|
|
||||||
|
const response = await testController.getPortForwards() as any;
|
||||||
|
const forwards = response?.data || [];
|
||||||
|
|
||||||
|
console.log(`Total Port Forwards: ${forwards.length}`);
|
||||||
|
|
||||||
|
for (const fwd of forwards) {
|
||||||
|
const enabled = fwd.enabled !== false ? 'ON' : 'OFF';
|
||||||
|
console.log('');
|
||||||
|
console.log(` [${enabled}] ${fwd.name || 'Unnamed'}`);
|
||||||
|
console.log(` External: ${fwd.dst_port || 'N/A'} -> Internal: ${fwd.fwd}:${fwd.fwd_port || fwd.dst_port}`);
|
||||||
|
console.log(` Protocol: ${fwd.proto || 'tcp_udp'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forwards.length === 0) {
|
||||||
|
console.log(' No port forwards configured');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('INFO - Connected Clients Summary', async () => {
|
||||||
|
printHeader('CLIENTS SUMMARY');
|
||||||
|
|
||||||
|
const allClients = await testController.clientManager.listActiveClients();
|
||||||
|
const wirelessClients = await testController.clientManager.getWirelessClients();
|
||||||
|
const wiredClients = await testController.clientManager.getWiredClients();
|
||||||
|
|
||||||
|
console.log(`Total Active Clients: ${allClients.length}`);
|
||||||
|
console.log(` - Wireless: ${wirelessClients.length}`);
|
||||||
|
console.log(` - Wired: ${wiredClients.length}`);
|
||||||
|
|
||||||
|
// Calculate total bandwidth
|
||||||
|
let totalTx = 0;
|
||||||
|
let totalRx = 0;
|
||||||
|
for (const client of allClients) {
|
||||||
|
totalTx += client.tx_bytes || 0;
|
||||||
|
totalRx += client.rx_bytes || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log(`Total Data Transfer:`);
|
||||||
|
console.log(` - Upload (TX): ${formatBytes(totalTx)}`);
|
||||||
|
console.log(` - Download (RX): ${formatBytes(totalRx)}`);
|
||||||
|
console.log(` - Combined: ${formatBytes(totalTx + totalRx)}`);
|
||||||
|
|
||||||
|
// Top 5 clients by data usage
|
||||||
|
printSubHeader('Top 5 Clients by Data Usage');
|
||||||
|
const sortedClients = [...allClients].sort((a, b) =>
|
||||||
|
(b.getDataUsage() || 0) - (a.getDataUsage() || 0)
|
||||||
|
).slice(0, 5);
|
||||||
|
|
||||||
|
for (const client of sortedClients) {
|
||||||
|
const usage = client.getDataUsage();
|
||||||
|
console.log(` ${client.getDisplayName()}`);
|
||||||
|
console.log(` IP: ${client.ip || 'N/A'} | Usage: ${formatBytes(usage)}`);
|
||||||
|
console.log(` Type: ${client.getConnectionType()}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('INFO - System Health', async () => {
|
||||||
|
printHeader('SYSTEM HEALTH');
|
||||||
|
|
||||||
|
const response = await testController.getHealth() as any;
|
||||||
|
const healthData = response?.data || [];
|
||||||
|
|
||||||
|
for (const subsystem of healthData) {
|
||||||
|
const status = subsystem.status === 'ok' ? 'OK' : subsystem.status?.toUpperCase() || 'UNKNOWN';
|
||||||
|
console.log(` [${status}] ${subsystem.subsystem}`);
|
||||||
|
|
||||||
|
if (subsystem.num_user !== undefined) {
|
||||||
|
console.log(` Users: ${subsystem.num_user}`);
|
||||||
|
}
|
||||||
|
if (subsystem.num_guest !== undefined) {
|
||||||
|
console.log(` Guests: ${subsystem.num_guest}`);
|
||||||
|
}
|
||||||
|
if (subsystem.num_ap !== undefined) {
|
||||||
|
console.log(` APs: ${subsystem.num_ap}`);
|
||||||
|
}
|
||||||
|
if (subsystem.num_adopted !== undefined) {
|
||||||
|
console.log(` Adopted: ${subsystem.num_adopted}`);
|
||||||
|
}
|
||||||
|
if (subsystem.tx_bytes_r !== undefined) {
|
||||||
|
console.log(` TX Rate: ${formatBytes(subsystem.tx_bytes_r)}/s`);
|
||||||
|
}
|
||||||
|
if (subsystem.rx_bytes_r !== undefined) {
|
||||||
|
console.log(` RX Rate: ${formatBytes(subsystem.rx_bytes_r)}/s`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('INFO - Recent Alerts', async () => {
|
||||||
|
printHeader('RECENT ALERTS');
|
||||||
|
|
||||||
|
const response = await testController.getAlerts() as any;
|
||||||
|
const alerts = response?.data || [];
|
||||||
|
|
||||||
|
console.log(`Total Alerts: ${alerts.length}`);
|
||||||
|
|
||||||
|
// Show last 10 alerts
|
||||||
|
const recentAlerts = alerts.slice(0, 10);
|
||||||
|
|
||||||
|
for (const alert of recentAlerts) {
|
||||||
|
const time = alert.time ? new Date(alert.time).toLocaleString() : 'Unknown time';
|
||||||
|
const archived = alert.archived ? '[ARCHIVED]' : '';
|
||||||
|
console.log('');
|
||||||
|
console.log(` ${time} ${archived}`);
|
||||||
|
console.log(` Type: ${alert.key || 'N/A'}`);
|
||||||
|
if (alert.msg) {
|
||||||
|
console.log(` Message: ${alert.msg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alerts.length === 0) {
|
||||||
|
console.log(' No alerts');
|
||||||
|
} else if (alerts.length > 10) {
|
||||||
|
console.log(` ... and ${alerts.length - 10} more alerts`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
143
test/test.network.ts
Normal file
143
test/test.network.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
|
import * as unifi from '../ts/index.js';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// NETWORK CONTROLLER API INTEGRATION TESTS (Local API)
|
||||||
|
// Tests may use live production keys to test specific features at scale.
|
||||||
|
// Make sure to avoid dangerous, destructive or security relevant operations.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const testQenv = new Qenv('./', './.nogit/');
|
||||||
|
|
||||||
|
let testController: unifi.UnifiController;
|
||||||
|
|
||||||
|
tap.test('setup - create UnifiController with API key', async () => {
|
||||||
|
const host = await testQenv.getEnvVarOnDemand('UNIFI_CONSOLE_IP');
|
||||||
|
const apiKey = await testQenv.getEnvVarOnDemand('UNIFI_NETWORK_DEV_KEY');
|
||||||
|
|
||||||
|
testController = new unifi.UnifiController({
|
||||||
|
host,
|
||||||
|
apiKey,
|
||||||
|
controllerType: 'unifi-os',
|
||||||
|
verifySsl: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// With API key, already authenticated
|
||||||
|
expect(testController.isAuthenticated()).toBeTrue();
|
||||||
|
console.log('UnifiController created with API key authentication');
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: List devices
|
||||||
|
tap.test('READ-ONLY - should list devices', async () => {
|
||||||
|
const devices = await testController.deviceManager.listDevices();
|
||||||
|
|
||||||
|
console.log(`Found ${devices.length} devices`);
|
||||||
|
expect(devices).toBeArray();
|
||||||
|
|
||||||
|
for (const device of devices) {
|
||||||
|
expect(device).toBeInstanceOf(unifi.UnifiDevice);
|
||||||
|
console.log(` - ${device.getDisplayName()} (${device.model}) - ${device.isOnline() ? 'online' : 'offline'}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: List access points
|
||||||
|
tap.test('READ-ONLY - should list access points', async () => {
|
||||||
|
const accessPoints = await testController.deviceManager.getAccessPoints();
|
||||||
|
|
||||||
|
console.log(`Found ${accessPoints.length} access points`);
|
||||||
|
expect(accessPoints).toBeArray();
|
||||||
|
|
||||||
|
for (const ap of accessPoints) {
|
||||||
|
console.log(` - AP: ${ap.getDisplayName()} (${ap.ip})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: List switches
|
||||||
|
tap.test('READ-ONLY - should list switches', async () => {
|
||||||
|
const switches = await testController.deviceManager.getSwitches();
|
||||||
|
|
||||||
|
console.log(`Found ${switches.length} switches`);
|
||||||
|
expect(switches).toBeArray();
|
||||||
|
|
||||||
|
for (const sw of switches) {
|
||||||
|
console.log(` - Switch: ${sw.getDisplayName()} (${sw.ip})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: List gateways
|
||||||
|
tap.test('READ-ONLY - should list gateways', async () => {
|
||||||
|
const gateways = await testController.deviceManager.getGateways();
|
||||||
|
|
||||||
|
console.log(`Found ${gateways.length} gateways`);
|
||||||
|
expect(gateways).toBeArray();
|
||||||
|
|
||||||
|
for (const gw of gateways) {
|
||||||
|
console.log(` - Gateway: ${gw.getDisplayName()} (${gw.ip})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: List active clients
|
||||||
|
tap.test('READ-ONLY - should list active clients', async () => {
|
||||||
|
const clients = await testController.clientManager.listActiveClients();
|
||||||
|
|
||||||
|
console.log(`Found ${clients.length} active clients`);
|
||||||
|
expect(clients).toBeArray();
|
||||||
|
|
||||||
|
for (const client of clients.slice(0, 10)) {
|
||||||
|
console.log(` - ${client.getDisplayName()} (${client.ip || 'no IP'}) - ${client.getConnectionType()}`);
|
||||||
|
}
|
||||||
|
if (clients.length > 10) {
|
||||||
|
console.log(` ... and ${clients.length - 10} more`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: List wireless clients
|
||||||
|
tap.test('READ-ONLY - should list wireless clients', async () => {
|
||||||
|
const wirelessClients = await testController.clientManager.getWirelessClients();
|
||||||
|
|
||||||
|
console.log(`Found ${wirelessClients.length} wireless clients`);
|
||||||
|
expect(wirelessClients).toBeArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: List wired clients
|
||||||
|
tap.test('READ-ONLY - should list wired clients', async () => {
|
||||||
|
const wiredClients = await testController.clientManager.getWiredClients();
|
||||||
|
|
||||||
|
console.log(`Found ${wiredClients.length} wired clients`);
|
||||||
|
expect(wiredClients).toBeArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: Get system info
|
||||||
|
tap.test('READ-ONLY - should get system info', async () => {
|
||||||
|
const sysInfo = await testController.getSystemInfo();
|
||||||
|
|
||||||
|
console.log('System info retrieved');
|
||||||
|
expect(sysInfo).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: Get health
|
||||||
|
tap.test('READ-ONLY - should get health status', async () => {
|
||||||
|
const health = await testController.getHealth();
|
||||||
|
|
||||||
|
console.log('Health status retrieved');
|
||||||
|
expect(health).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: Get WLANs
|
||||||
|
tap.test('READ-ONLY - should list WLANs', async () => {
|
||||||
|
const wlans = await testController.getWlans();
|
||||||
|
|
||||||
|
console.log('WLANs retrieved');
|
||||||
|
expect(wlans).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: Get networks
|
||||||
|
tap.test('READ-ONLY - should list networks', async () => {
|
||||||
|
const networks = await testController.getNetworks();
|
||||||
|
|
||||||
|
console.log('Networks retrieved');
|
||||||
|
expect(networks).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
147
test/test.protect.ts
Normal file
147
test/test.protect.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
|
import * as unifi from '../ts/index.js';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PROTECT API INTEGRATION TESTS (Local NVR API)
|
||||||
|
// Tests may use live production keys to test specific features at scale.
|
||||||
|
// Make sure to avoid dangerous, destructive or security relevant operations.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const testQenv = new Qenv('./', './.nogit/');
|
||||||
|
|
||||||
|
let testProtect: unifi.UnifiProtect;
|
||||||
|
|
||||||
|
tap.test('setup - create UnifiProtect with API key', async () => {
|
||||||
|
const host = await testQenv.getEnvVarOnDemand('UNIFI_CONSOLE_IP');
|
||||||
|
const apiKey = await testQenv.getEnvVarOnDemand('UNIFI_PROTECT_DEV_KEY');
|
||||||
|
|
||||||
|
testProtect = new unifi.UnifiProtect({
|
||||||
|
host,
|
||||||
|
apiKey,
|
||||||
|
verifySsl: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(testProtect.isAuthenticated()).toBeTrue();
|
||||||
|
console.log('UnifiProtect created with API key authentication');
|
||||||
|
|
||||||
|
// Load bootstrap data
|
||||||
|
await testProtect.refreshBootstrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: Get NVR info
|
||||||
|
tap.test('READ-ONLY - should get NVR info', async () => {
|
||||||
|
const nvrInfo = testProtect.getNvrInfo();
|
||||||
|
|
||||||
|
if (nvrInfo) {
|
||||||
|
console.log(`NVR: ${nvrInfo.name} (${nvrInfo.type})`);
|
||||||
|
console.log(` Firmware: ${nvrInfo.firmwareVersion}`);
|
||||||
|
console.log(` Cloud connected: ${testProtect.isCloudConnected()}`);
|
||||||
|
} else {
|
||||||
|
console.log('NVR info not available in bootstrap (may be different API version)');
|
||||||
|
}
|
||||||
|
// Don't fail - NVR info may not be present in all API versions
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: Get storage info
|
||||||
|
tap.test('READ-ONLY - should get storage info', async () => {
|
||||||
|
const storageInfo = testProtect.getStorageInfo();
|
||||||
|
|
||||||
|
if (storageInfo && storageInfo.totalSize) {
|
||||||
|
const totalGB = (storageInfo.totalSize / 1024 / 1024 / 1024).toFixed(2);
|
||||||
|
const usedGB = storageInfo.usedSpace ? (storageInfo.usedSpace / 1024 / 1024 / 1024).toFixed(2) : 'unknown';
|
||||||
|
console.log(`Storage: ${usedGB} GB used of ${totalGB} GB`);
|
||||||
|
} else {
|
||||||
|
console.log('Storage info not available');
|
||||||
|
}
|
||||||
|
// Don't fail - storage info may not be present
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: List cameras
|
||||||
|
tap.test('READ-ONLY - should list cameras', async () => {
|
||||||
|
const cameras = await testProtect.cameraManager.listCameras();
|
||||||
|
|
||||||
|
console.log(`Found ${cameras.length} cameras`);
|
||||||
|
expect(cameras).toBeArray();
|
||||||
|
|
||||||
|
for (const camera of cameras) {
|
||||||
|
expect(camera).toBeInstanceOf(unifi.UnifiCamera);
|
||||||
|
console.log(` - ${camera.name} (${camera.type}) - ${camera.isOnline() ? 'online' : 'offline'}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: List online cameras
|
||||||
|
tap.test('READ-ONLY - should list online cameras', async () => {
|
||||||
|
const onlineCameras = await testProtect.cameraManager.getOnlineCameras();
|
||||||
|
|
||||||
|
console.log(`Found ${onlineCameras.length} online cameras`);
|
||||||
|
expect(onlineCameras).toBeArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: List offline cameras
|
||||||
|
tap.test('READ-ONLY - should list offline cameras', async () => {
|
||||||
|
const offlineCameras = await testProtect.cameraManager.getOfflineCameras();
|
||||||
|
|
||||||
|
console.log(`Found ${offlineCameras.length} offline cameras`);
|
||||||
|
expect(offlineCameras).toBeArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: List doorbells
|
||||||
|
tap.test('READ-ONLY - should list doorbells', async () => {
|
||||||
|
const doorbells = await testProtect.cameraManager.getDoorbells();
|
||||||
|
|
||||||
|
console.log(`Found ${doorbells.length} doorbells`);
|
||||||
|
expect(doorbells).toBeArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: List smart detect cameras
|
||||||
|
tap.test('READ-ONLY - should list smart detect cameras', async () => {
|
||||||
|
const smartCameras = await testProtect.cameraManager.getSmartDetectCameras();
|
||||||
|
|
||||||
|
console.log(`Found ${smartCameras.length} cameras with smart detect`);
|
||||||
|
expect(smartCameras).toBeArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: List cameras with recent motion
|
||||||
|
tap.test('READ-ONLY - should list cameras with recent motion', async () => {
|
||||||
|
const recentMotion = await testProtect.cameraManager.getCamerasWithRecentMotion(300);
|
||||||
|
|
||||||
|
console.log(`Found ${recentMotion.length} cameras with motion in last 5 minutes`);
|
||||||
|
expect(recentMotion).toBeArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: Get motion events
|
||||||
|
tap.test('READ-ONLY - should get recent motion events', async () => {
|
||||||
|
const events = await testProtect.cameraManager.getAllMotionEvents({ limit: 10 });
|
||||||
|
|
||||||
|
// API might return object or array, handle both
|
||||||
|
const eventsArray = Array.isArray(events) ? events : [];
|
||||||
|
console.log(`Retrieved ${eventsArray.length} recent motion events`);
|
||||||
|
expect(eventsArray).toBeArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: Get lights
|
||||||
|
tap.test('READ-ONLY - should get lights', async () => {
|
||||||
|
const lights = testProtect.getLights();
|
||||||
|
|
||||||
|
console.log(`Found ${lights.length} lights`);
|
||||||
|
expect(lights).toBeArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: Get sensors
|
||||||
|
tap.test('READ-ONLY - should get sensors', async () => {
|
||||||
|
const sensors = testProtect.getSensors();
|
||||||
|
|
||||||
|
console.log(`Found ${sensors.length} sensors`);
|
||||||
|
expect(sensors).toBeArray();
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: Get liveviews
|
||||||
|
tap.test('READ-ONLY - should get liveviews', async () => {
|
||||||
|
const liveviews = await testProtect.getLiveviews();
|
||||||
|
|
||||||
|
console.log('Liveviews retrieved');
|
||||||
|
expect(liveviews).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
42
test/test.site.ts
Normal file
42
test/test.site.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
|
import * as unifi from '../ts/index.js';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SITE API INTEGRATION TESTS (Local Controller)
|
||||||
|
// Tests may use live production keys to test specific features at scale.
|
||||||
|
// Make sure to avoid dangerous, destructive or security relevant operations.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const testQenv = new Qenv('./', './.nogit/');
|
||||||
|
|
||||||
|
let testController: unifi.UnifiController;
|
||||||
|
|
||||||
|
tap.test('setup - create UnifiController with API key', async () => {
|
||||||
|
const host = await testQenv.getEnvVarOnDemand('UNIFI_CONSOLE_IP');
|
||||||
|
const apiKey = await testQenv.getEnvVarOnDemand('UNIFI_NETWORK_DEV_KEY');
|
||||||
|
|
||||||
|
testController = new unifi.UnifiController({
|
||||||
|
host,
|
||||||
|
apiKey,
|
||||||
|
controllerType: 'unifi-os',
|
||||||
|
verifySsl: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(testController.isAuthenticated()).toBeTrue();
|
||||||
|
console.log('UnifiController created with API key authentication');
|
||||||
|
});
|
||||||
|
|
||||||
|
// READ-ONLY: List sites
|
||||||
|
tap.test('READ-ONLY - should list sites', async () => {
|
||||||
|
const sites = await testController.listSites();
|
||||||
|
|
||||||
|
console.log(`Found ${sites.length} sites`);
|
||||||
|
expect(sites).toBeArray();
|
||||||
|
|
||||||
|
for (const site of sites) {
|
||||||
|
console.log(` - Site: ${site.name} (${site._id})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
181
test/test.ts
181
test/test.ts
@@ -1,8 +1,179 @@
|
|||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as unifi from '../ts/index.js'
|
import * as unifi from '../ts/index.js';
|
||||||
|
|
||||||
tap.test('first test', async () => {
|
// =============================================================================
|
||||||
console.log(unifi)
|
// UNIT TESTS - No credentials needed, tests class structure and exports
|
||||||
})
|
// =============================================================================
|
||||||
|
|
||||||
export default tap.start()
|
tap.test('should export all main classes', async () => {
|
||||||
|
// Entry point classes
|
||||||
|
expect(unifi.UnifiAccount).toBeDefined();
|
||||||
|
expect(unifi.UnifiController).toBeDefined();
|
||||||
|
expect(unifi.UnifiProtect).toBeDefined();
|
||||||
|
expect(unifi.UnifiAccess).toBeDefined();
|
||||||
|
|
||||||
|
// Manager classes
|
||||||
|
expect(unifi.SiteManager).toBeDefined();
|
||||||
|
expect(unifi.HostManager).toBeDefined();
|
||||||
|
expect(unifi.DeviceManager).toBeDefined();
|
||||||
|
expect(unifi.ClientManager).toBeDefined();
|
||||||
|
expect(unifi.CameraManager).toBeDefined();
|
||||||
|
expect(unifi.DoorManager).toBeDefined();
|
||||||
|
|
||||||
|
// Resource classes
|
||||||
|
expect(unifi.UnifiSite).toBeDefined();
|
||||||
|
expect(unifi.UnifiHost).toBeDefined();
|
||||||
|
expect(unifi.UnifiDevice).toBeDefined();
|
||||||
|
expect(unifi.UnifiClient).toBeDefined();
|
||||||
|
expect(unifi.UnifiCamera).toBeDefined();
|
||||||
|
expect(unifi.UnifiDoor).toBeDefined();
|
||||||
|
|
||||||
|
// HTTP client
|
||||||
|
expect(unifi.UnifiHttp).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should instantiate UnifiAccount', async () => {
|
||||||
|
const account = new unifi.UnifiAccount({
|
||||||
|
apiKey: 'test-api-key',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(account).toBeInstanceOf(unifi.UnifiAccount);
|
||||||
|
expect(account.siteManager).toBeInstanceOf(unifi.SiteManager);
|
||||||
|
expect(account.hostManager).toBeInstanceOf(unifi.HostManager);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should instantiate UnifiController', async () => {
|
||||||
|
const controller = new unifi.UnifiController({
|
||||||
|
host: '192.168.1.1',
|
||||||
|
username: 'admin',
|
||||||
|
password: 'password',
|
||||||
|
controllerType: 'unifi-os',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(controller).toBeInstanceOf(unifi.UnifiController);
|
||||||
|
expect(controller.deviceManager).toBeInstanceOf(unifi.DeviceManager);
|
||||||
|
expect(controller.clientManager).toBeInstanceOf(unifi.ClientManager);
|
||||||
|
expect(controller.isAuthenticated()).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should instantiate UnifiProtect', async () => {
|
||||||
|
const protect = new unifi.UnifiProtect({
|
||||||
|
host: '192.168.1.1',
|
||||||
|
username: 'admin',
|
||||||
|
password: 'password',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(protect).toBeInstanceOf(unifi.UnifiProtect);
|
||||||
|
expect(protect.cameraManager).toBeInstanceOf(unifi.CameraManager);
|
||||||
|
expect(protect.isAuthenticated()).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should instantiate UnifiAccess', async () => {
|
||||||
|
const access = new unifi.UnifiAccess({
|
||||||
|
host: '192.168.1.1',
|
||||||
|
token: 'test-bearer-token',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(access).toBeInstanceOf(unifi.UnifiAccess);
|
||||||
|
expect(access.doorManager).toBeInstanceOf(unifi.DoorManager);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create UnifiSite from API object', async () => {
|
||||||
|
const site = unifi.UnifiSite.createFromApiObject({
|
||||||
|
siteId: 'site-123',
|
||||||
|
name: 'Test Site',
|
||||||
|
description: 'A test site',
|
||||||
|
isDefault: true,
|
||||||
|
timezone: 'UTC',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(site.siteId).toEqual('site-123');
|
||||||
|
expect(site.name).toEqual('Test Site');
|
||||||
|
expect(site.isDefault).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create UnifiDevice from API object', async () => {
|
||||||
|
const device = unifi.UnifiDevice.createFromApiObject({
|
||||||
|
_id: 'dev-123',
|
||||||
|
mac: '00:11:22:33:44:55',
|
||||||
|
model: 'UAP-AC-Pro',
|
||||||
|
type: 'uap',
|
||||||
|
name: 'Living Room AP',
|
||||||
|
site_id: 'default',
|
||||||
|
adopted: true,
|
||||||
|
ip: '192.168.1.100',
|
||||||
|
state: 1,
|
||||||
|
version: '6.0.0',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(device._id).toEqual('dev-123');
|
||||||
|
expect(device.mac).toEqual('00:11:22:33:44:55');
|
||||||
|
expect(device.isOnline()).toBeTrue();
|
||||||
|
expect(device.isAccessPoint()).toBeTrue();
|
||||||
|
expect(device.isSwitch()).toBeFalse();
|
||||||
|
expect(device.getDisplayName()).toEqual('Living Room AP');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create UnifiClient from API object', async () => {
|
||||||
|
const client = unifi.UnifiClient.createFromApiObject({
|
||||||
|
_id: 'client-123',
|
||||||
|
mac: 'aa:bb:cc:dd:ee:ff',
|
||||||
|
site_id: 'default',
|
||||||
|
is_wired: false,
|
||||||
|
hostname: 'laptop',
|
||||||
|
ip: '192.168.1.50',
|
||||||
|
essid: 'HomeWiFi',
|
||||||
|
signal: -55,
|
||||||
|
tx_bytes: 1073741824,
|
||||||
|
rx_bytes: 2147483648,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(client._id).toEqual('client-123');
|
||||||
|
expect(client.isWireless()).toBeTrue();
|
||||||
|
expect(client.getConnectionType()).toEqual('Wireless (HomeWiFi)');
|
||||||
|
expect(client.getSignalQuality()).toEqual('Good');
|
||||||
|
expect(client.getDataUsage()).toEqual(3221225472);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create UnifiCamera from API object', async () => {
|
||||||
|
const camera = unifi.UnifiCamera.createFromApiObject({
|
||||||
|
id: 'cam-123',
|
||||||
|
mac: '00:11:22:33:44:66',
|
||||||
|
host: '192.168.1.101',
|
||||||
|
name: 'Front Door',
|
||||||
|
type: 'UVC-G4-Doorbell',
|
||||||
|
state: 'CONNECTED',
|
||||||
|
isConnected: true,
|
||||||
|
isRecording: true,
|
||||||
|
featureFlags: {
|
||||||
|
hasSmartDetect: true,
|
||||||
|
hasMic: true,
|
||||||
|
hasSpeaker: true,
|
||||||
|
},
|
||||||
|
lastMotion: Date.now() - 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(camera.id).toEqual('cam-123');
|
||||||
|
expect(camera.isOnline()).toBeTrue();
|
||||||
|
expect(camera.isDoorbell()).toBeTrue();
|
||||||
|
expect(camera.hasSmartDetect()).toBeTrue();
|
||||||
|
expect(camera.hasRecentMotion(60)).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create UnifiDoor from API object', async () => {
|
||||||
|
const door = unifi.UnifiDoor.createFromApiObject({
|
||||||
|
unique_id: 'door-123',
|
||||||
|
name: 'Main Entrance',
|
||||||
|
alias: 'Front Door',
|
||||||
|
door_lock_relay_status: 'lock',
|
||||||
|
door_position_status: 'close',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(door.unique_id).toEqual('door-123');
|
||||||
|
expect(door.getDisplayName()).toEqual('Front Door');
|
||||||
|
expect(door.isLocked()).toBeTrue();
|
||||||
|
expect(door.isClosed()).toBeTrue();
|
||||||
|
expect(door.getStatus()).toEqual('Locked, Closed');
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
|
|||||||
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* autocreated commitinfo by @push.rocks/commitinfo
|
||||||
|
*/
|
||||||
|
export const commitinfo = {
|
||||||
|
name: '@apiclient.xyz/unifi',
|
||||||
|
version: '1.1.0',
|
||||||
|
description: 'an unofficial unifi api package'
|
||||||
|
}
|
||||||
275
ts/classes.camera.ts
Normal file
275
ts/classes.camera.ts
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
import type { UnifiProtect } from './classes.unifi-protect.js';
|
||||||
|
import type {
|
||||||
|
IProtectCamera,
|
||||||
|
IProtectCameraChannel,
|
||||||
|
IProtectRecordingSettings,
|
||||||
|
IProtectSmartDetectSettings,
|
||||||
|
IProtectIspSettings,
|
||||||
|
IProtectFeatureFlags,
|
||||||
|
IProtectCameraStats,
|
||||||
|
} from './interfaces/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a UniFi Protect camera
|
||||||
|
*/
|
||||||
|
export class UnifiCamera implements IProtectCamera {
|
||||||
|
/** Reference to parent protect instance */
|
||||||
|
private protect?: UnifiProtect;
|
||||||
|
|
||||||
|
// IProtectCamera properties
|
||||||
|
public id: string;
|
||||||
|
public mac: string;
|
||||||
|
public host: string;
|
||||||
|
public name: string;
|
||||||
|
public type: string;
|
||||||
|
public modelKey?: string;
|
||||||
|
public state: 'CONNECTED' | 'DISCONNECTED' | 'CONNECTING' | 'ADOPTING' | 'MANAGED';
|
||||||
|
public hardwareRevision?: string;
|
||||||
|
public firmwareVersion?: string;
|
||||||
|
public firmwareBuild?: string;
|
||||||
|
public isUpdating?: boolean;
|
||||||
|
public isAdopting?: boolean;
|
||||||
|
public isManaged?: boolean;
|
||||||
|
public isConnected?: boolean;
|
||||||
|
public isRecording?: boolean;
|
||||||
|
public isMotionDetected?: boolean;
|
||||||
|
public isDark?: boolean;
|
||||||
|
public recordingSettings?: IProtectRecordingSettings;
|
||||||
|
public smartDetectSettings?: IProtectSmartDetectSettings;
|
||||||
|
public ispSettings?: IProtectIspSettings;
|
||||||
|
public micVolume?: number;
|
||||||
|
public speakerVolume?: number;
|
||||||
|
public lastMotion?: number;
|
||||||
|
public lastRing?: number;
|
||||||
|
public uptime?: number;
|
||||||
|
public connectedSince?: number;
|
||||||
|
public upSince?: number;
|
||||||
|
public lastSeen?: number;
|
||||||
|
public channels?: IProtectCameraChannel[];
|
||||||
|
public featureFlags?: IProtectFeatureFlags;
|
||||||
|
public stats?: IProtectCameraStats;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.id = '';
|
||||||
|
this.mac = '';
|
||||||
|
this.host = '';
|
||||||
|
this.name = '';
|
||||||
|
this.type = '';
|
||||||
|
this.state = 'DISCONNECTED';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a camera instance from API response object
|
||||||
|
*/
|
||||||
|
public static createFromApiObject(
|
||||||
|
apiObject: IProtectCamera,
|
||||||
|
protect?: UnifiProtect
|
||||||
|
): UnifiCamera {
|
||||||
|
const camera = new UnifiCamera();
|
||||||
|
Object.assign(camera, apiObject);
|
||||||
|
camera.protect = protect;
|
||||||
|
return camera;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the raw API object representation
|
||||||
|
*/
|
||||||
|
public toApiObject(): IProtectCamera {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
mac: this.mac,
|
||||||
|
host: this.host,
|
||||||
|
name: this.name,
|
||||||
|
type: this.type,
|
||||||
|
modelKey: this.modelKey,
|
||||||
|
state: this.state,
|
||||||
|
hardwareRevision: this.hardwareRevision,
|
||||||
|
firmwareVersion: this.firmwareVersion,
|
||||||
|
firmwareBuild: this.firmwareBuild,
|
||||||
|
isUpdating: this.isUpdating,
|
||||||
|
isAdopting: this.isAdopting,
|
||||||
|
isManaged: this.isManaged,
|
||||||
|
isConnected: this.isConnected,
|
||||||
|
isRecording: this.isRecording,
|
||||||
|
isMotionDetected: this.isMotionDetected,
|
||||||
|
isDark: this.isDark,
|
||||||
|
recordingSettings: this.recordingSettings,
|
||||||
|
smartDetectSettings: this.smartDetectSettings,
|
||||||
|
ispSettings: this.ispSettings,
|
||||||
|
micVolume: this.micVolume,
|
||||||
|
speakerVolume: this.speakerVolume,
|
||||||
|
lastMotion: this.lastMotion,
|
||||||
|
lastRing: this.lastRing,
|
||||||
|
uptime: this.uptime,
|
||||||
|
connectedSince: this.connectedSince,
|
||||||
|
upSince: this.upSince,
|
||||||
|
lastSeen: this.lastSeen,
|
||||||
|
channels: this.channels,
|
||||||
|
featureFlags: this.featureFlags,
|
||||||
|
stats: this.stats,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if camera is online
|
||||||
|
*/
|
||||||
|
public isOnline(): boolean {
|
||||||
|
return this.state === 'CONNECTED' && this.isConnected === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if camera is a doorbell
|
||||||
|
*/
|
||||||
|
public isDoorbell(): boolean {
|
||||||
|
return this.type.toLowerCase().includes('doorbell');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if camera has smart detection capability
|
||||||
|
*/
|
||||||
|
public hasSmartDetect(): boolean {
|
||||||
|
return this.featureFlags?.hasSmartDetect === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get RTSP stream URL for a channel
|
||||||
|
*/
|
||||||
|
public getRtspUrl(channelId: number = 0): string | null {
|
||||||
|
const channel = this.channels?.find((c) => c.id === channelId);
|
||||||
|
if (!channel || !channel.isRtspEnabled || !channel.rtspAlias) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTSP URL format: rtsp://{host}:7447/{rtspAlias}
|
||||||
|
return `rtsp://${this.host}:7447/${channel.rtspAlias}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the high quality channel
|
||||||
|
*/
|
||||||
|
public getHighQualityChannel(): IProtectCameraChannel | null {
|
||||||
|
// Channel 0 is typically the highest quality
|
||||||
|
return this.channels?.find((c) => c.id === 0) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the medium quality channel
|
||||||
|
*/
|
||||||
|
public getMediumQualityChannel(): IProtectCameraChannel | null {
|
||||||
|
// Channel 1 is typically medium quality
|
||||||
|
return this.channels?.find((c) => c.id === 1) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the low quality channel
|
||||||
|
*/
|
||||||
|
public getLowQualityChannel(): IProtectCameraChannel | null {
|
||||||
|
// Channel 2 is typically low quality
|
||||||
|
return this.channels?.find((c) => c.id === 2) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update camera settings
|
||||||
|
*/
|
||||||
|
public async updateSettings(settings: Partial<IProtectCamera>): Promise<void> {
|
||||||
|
if (!this.protect) {
|
||||||
|
throw new Error('Cannot update camera: no protect reference');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.protect.request('PATCH', `/cameras/${this.id}`, settings);
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
Object.assign(this, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set recording mode
|
||||||
|
*/
|
||||||
|
public async setRecordingMode(
|
||||||
|
mode: 'always' | 'detections' | 'never' | 'schedule'
|
||||||
|
): Promise<void> {
|
||||||
|
await this.updateSettings({
|
||||||
|
recordingSettings: {
|
||||||
|
...this.recordingSettings,
|
||||||
|
mode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable/disable smart detection types
|
||||||
|
*/
|
||||||
|
public async setSmartDetectTypes(types: string[]): Promise<void> {
|
||||||
|
await this.updateSettings({
|
||||||
|
smartDetectSettings: {
|
||||||
|
...this.smartDetectSettings,
|
||||||
|
objectTypes: types,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set microphone volume
|
||||||
|
*/
|
||||||
|
public async setMicVolume(volume: number): Promise<void> {
|
||||||
|
const clampedVolume = Math.max(0, Math.min(100, volume));
|
||||||
|
await this.updateSettings({ micVolume: clampedVolume });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set speaker volume
|
||||||
|
*/
|
||||||
|
public async setSpeakerVolume(volume: number): Promise<void> {
|
||||||
|
const clampedVolume = Math.max(0, Math.min(100, volume));
|
||||||
|
await this.updateSettings({ speakerVolume: clampedVolume });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename the camera
|
||||||
|
*/
|
||||||
|
public async rename(newName: string): Promise<void> {
|
||||||
|
await this.updateSettings({ name: newName });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart the camera
|
||||||
|
*/
|
||||||
|
public async restart(): Promise<void> {
|
||||||
|
if (!this.protect) {
|
||||||
|
throw new Error('Cannot restart camera: no protect reference');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.protect.request('POST', `/cameras/${this.id}/reboot`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get snapshot URL
|
||||||
|
*/
|
||||||
|
public getSnapshotUrl(): string {
|
||||||
|
if (!this.protect) {
|
||||||
|
throw new Error('Cannot get snapshot URL: no protect reference');
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/proxy/protect/api/cameras/${this.id}/snapshot`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if camera has motion in the last N seconds
|
||||||
|
*/
|
||||||
|
public hasRecentMotion(seconds: number = 60): boolean {
|
||||||
|
if (!this.lastMotion) return false;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const motionAge = (now - this.lastMotion) / 1000;
|
||||||
|
return motionAge <= seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get time since last motion in seconds
|
||||||
|
*/
|
||||||
|
public getTimeSinceLastMotion(): number | null {
|
||||||
|
if (!this.lastMotion) return null;
|
||||||
|
|
||||||
|
return Math.floor((Date.now() - this.lastMotion) / 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
227
ts/classes.cameramanager.ts
Normal file
227
ts/classes.cameramanager.ts
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
import type { UnifiProtect } from './classes.unifi-protect.js';
|
||||||
|
import { UnifiCamera } from './classes.camera.js';
|
||||||
|
import type { IProtectCamera, IProtectMotionEvent } from './interfaces/index.js';
|
||||||
|
import { logger } from './unifi.logger.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager for UniFi Protect cameras
|
||||||
|
*/
|
||||||
|
export class CameraManager {
|
||||||
|
private protect: UnifiProtect;
|
||||||
|
|
||||||
|
constructor(protect: UnifiProtect) {
|
||||||
|
this.protect = protect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all cameras
|
||||||
|
*/
|
||||||
|
public async listCameras(): Promise<UnifiCamera[]> {
|
||||||
|
logger.log('debug', 'Fetching cameras from Protect');
|
||||||
|
|
||||||
|
const cameras = this.protect.getCamerasFromBootstrap();
|
||||||
|
return cameras.map((cameraData) =>
|
||||||
|
UnifiCamera.createFromApiObject(cameraData, this.protect)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a camera by ID
|
||||||
|
*/
|
||||||
|
public async getCameraById(cameraId: string): Promise<UnifiCamera | null> {
|
||||||
|
const cameras = await this.listCameras();
|
||||||
|
return cameras.find((camera) => camera.id === cameraId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a camera by MAC address
|
||||||
|
*/
|
||||||
|
public async getCameraByMac(mac: string): Promise<UnifiCamera | null> {
|
||||||
|
const normalizedMac = mac.toLowerCase().replace(/[:-]/g, ':');
|
||||||
|
const cameras = await this.listCameras();
|
||||||
|
return cameras.find((camera) => camera.mac.toLowerCase() === normalizedMac) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a camera by name
|
||||||
|
*/
|
||||||
|
public async getCameraByName(name: string): Promise<UnifiCamera | null> {
|
||||||
|
const cameras = await this.listCameras();
|
||||||
|
return cameras.find(
|
||||||
|
(camera) => camera.name.toLowerCase() === name.toLowerCase()
|
||||||
|
) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get online cameras only
|
||||||
|
*/
|
||||||
|
public async getOnlineCameras(): Promise<UnifiCamera[]> {
|
||||||
|
const cameras = await this.listCameras();
|
||||||
|
return cameras.filter((camera) => camera.isOnline());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get offline cameras only
|
||||||
|
*/
|
||||||
|
public async getOfflineCameras(): Promise<UnifiCamera[]> {
|
||||||
|
const cameras = await this.listCameras();
|
||||||
|
return cameras.filter((camera) => !camera.isOnline());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get doorbells only
|
||||||
|
*/
|
||||||
|
public async getDoorbells(): Promise<UnifiCamera[]> {
|
||||||
|
const cameras = await this.listCameras();
|
||||||
|
return cameras.filter((camera) => camera.isDoorbell());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cameras with smart detection
|
||||||
|
*/
|
||||||
|
public async getSmartDetectCameras(): Promise<UnifiCamera[]> {
|
||||||
|
const cameras = await this.listCameras();
|
||||||
|
return cameras.filter((camera) => camera.hasSmartDetect());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cameras currently recording
|
||||||
|
*/
|
||||||
|
public async getRecordingCameras(): Promise<UnifiCamera[]> {
|
||||||
|
const cameras = await this.listCameras();
|
||||||
|
return cameras.filter((camera) => camera.isRecording === true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cameras with recent motion
|
||||||
|
*/
|
||||||
|
public async getCamerasWithRecentMotion(seconds: number = 60): Promise<UnifiCamera[]> {
|
||||||
|
const cameras = await this.listCameras();
|
||||||
|
return cameras.filter((camera) => camera.hasRecentMotion(seconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update camera settings
|
||||||
|
*/
|
||||||
|
public async updateCamera(
|
||||||
|
cameraId: string,
|
||||||
|
settings: Partial<IProtectCamera>
|
||||||
|
): Promise<void> {
|
||||||
|
logger.log('info', `Updating camera: ${cameraId}`);
|
||||||
|
|
||||||
|
await this.protect.request('PATCH', `/cameras/${cameraId}`, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get motion events for a camera
|
||||||
|
*/
|
||||||
|
public async getMotionEvents(
|
||||||
|
cameraId: string,
|
||||||
|
options: { start?: number; end?: number; limit?: number } = {}
|
||||||
|
): Promise<IProtectMotionEvent[]> {
|
||||||
|
logger.log('debug', `Fetching motion events for camera: ${cameraId}`);
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('cameras', cameraId);
|
||||||
|
if (options.start) params.append('start', options.start.toString());
|
||||||
|
if (options.end) params.append('end', options.end.toString());
|
||||||
|
if (options.limit) params.append('limit', options.limit.toString());
|
||||||
|
|
||||||
|
const response = await this.protect.request<IProtectMotionEvent[] | { data: IProtectMotionEvent[] }>(
|
||||||
|
'GET',
|
||||||
|
`/events?${params.toString()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle both array and {data: [...]} response formats
|
||||||
|
return Array.isArray(response) ? response : (response?.data || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all recent motion events
|
||||||
|
*/
|
||||||
|
public async getAllMotionEvents(
|
||||||
|
options: { start?: number; end?: number; limit?: number } = {}
|
||||||
|
): Promise<IProtectMotionEvent[]> {
|
||||||
|
logger.log('debug', 'Fetching all motion events');
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (options.start) params.append('start', options.start.toString());
|
||||||
|
if (options.end) params.append('end', options.end.toString());
|
||||||
|
if (options.limit) params.append('limit', options.limit.toString());
|
||||||
|
|
||||||
|
const queryString = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
const response = await this.protect.request<IProtectMotionEvent[] | { data: IProtectMotionEvent[] }>(
|
||||||
|
'GET',
|
||||||
|
`/events${queryString}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle both array and {data: [...]} response formats
|
||||||
|
return Array.isArray(response) ? response : (response?.data || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get snapshot for a camera
|
||||||
|
*/
|
||||||
|
public async getSnapshot(
|
||||||
|
cameraId: string,
|
||||||
|
options: { width?: number; height?: number; ts?: number } = {}
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
logger.log('debug', `Fetching snapshot for camera: ${cameraId}`);
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (options.width) params.append('w', options.width.toString());
|
||||||
|
if (options.height) params.append('h', options.height.toString());
|
||||||
|
if (options.ts) params.append('ts', options.ts.toString());
|
||||||
|
|
||||||
|
const queryString = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
|
||||||
|
// Note: This returns binary data, may need different handling
|
||||||
|
const response = await this.protect.request<ArrayBuffer>(
|
||||||
|
'GET',
|
||||||
|
`/cameras/${cameraId}/snapshot${queryString}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set recording mode for all cameras
|
||||||
|
*/
|
||||||
|
public async setAllRecordingMode(
|
||||||
|
mode: 'always' | 'detections' | 'never' | 'schedule'
|
||||||
|
): Promise<void> {
|
||||||
|
logger.log('info', `Setting all cameras to recording mode: ${mode}`);
|
||||||
|
|
||||||
|
const cameras = await this.listCameras();
|
||||||
|
const updatePromises = cameras.map((camera) =>
|
||||||
|
this.updateCamera(camera.id, {
|
||||||
|
recordingSettings: {
|
||||||
|
...camera.recordingSettings,
|
||||||
|
mode,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(updatePromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adopt a camera
|
||||||
|
*/
|
||||||
|
public async adoptCamera(mac: string): Promise<void> {
|
||||||
|
logger.log('info', `Adopting camera: ${mac}`);
|
||||||
|
|
||||||
|
await this.protect.request('POST', '/cameras/adopt', {
|
||||||
|
mac: mac.toLowerCase(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a camera
|
||||||
|
*/
|
||||||
|
public async removeCamera(cameraId: string): Promise<void> {
|
||||||
|
logger.log('info', `Removing camera: ${cameraId}`);
|
||||||
|
|
||||||
|
await this.protect.request('DELETE', `/cameras/${cameraId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
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`;
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
255
ts/classes.device.ts
Normal file
255
ts/classes.device.ts
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import type { UnifiController } from './classes.unifi-controller.js';
|
||||||
|
import type { INetworkDevice, IPortConfig } from './interfaces/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a UniFi network device (AP, switch, gateway, etc.)
|
||||||
|
*/
|
||||||
|
export class UnifiDevice implements INetworkDevice {
|
||||||
|
/** Reference to parent controller */
|
||||||
|
private controller?: UnifiController;
|
||||||
|
/** Site ID for API calls */
|
||||||
|
private siteId?: string;
|
||||||
|
|
||||||
|
// INetworkDevice properties
|
||||||
|
public _id: string;
|
||||||
|
public mac: string;
|
||||||
|
public model: string;
|
||||||
|
public type: string;
|
||||||
|
public name?: string;
|
||||||
|
public site_id: string;
|
||||||
|
public adopted: boolean;
|
||||||
|
public ip: string;
|
||||||
|
public state: number;
|
||||||
|
public serial?: string;
|
||||||
|
public version?: string;
|
||||||
|
public uptime?: number;
|
||||||
|
public last_seen?: number;
|
||||||
|
public upgradable?: boolean;
|
||||||
|
public upgrade_to_firmware?: string;
|
||||||
|
public config_network?: {
|
||||||
|
type?: string;
|
||||||
|
ip?: string;
|
||||||
|
};
|
||||||
|
public ethernet_table?: Array<{
|
||||||
|
name: string;
|
||||||
|
mac: string;
|
||||||
|
num_port?: number;
|
||||||
|
}>;
|
||||||
|
public port_overrides?: Array<{
|
||||||
|
port_idx: number;
|
||||||
|
name?: string;
|
||||||
|
poe_mode?: string;
|
||||||
|
}>;
|
||||||
|
public sys_stats?: {
|
||||||
|
loadavg_1?: number;
|
||||||
|
loadavg_5?: number;
|
||||||
|
loadavg_15?: number;
|
||||||
|
mem_total?: number;
|
||||||
|
mem_used?: number;
|
||||||
|
};
|
||||||
|
public led_override?: string;
|
||||||
|
public led_override_color?: string;
|
||||||
|
public led_override_color_brightness?: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._id = '';
|
||||||
|
this.mac = '';
|
||||||
|
this.model = '';
|
||||||
|
this.type = '';
|
||||||
|
this.site_id = '';
|
||||||
|
this.adopted = false;
|
||||||
|
this.ip = '';
|
||||||
|
this.state = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a device instance from API response object
|
||||||
|
*/
|
||||||
|
public static createFromApiObject(
|
||||||
|
apiObject: INetworkDevice,
|
||||||
|
controller?: UnifiController,
|
||||||
|
siteId?: string
|
||||||
|
): UnifiDevice {
|
||||||
|
const device = new UnifiDevice();
|
||||||
|
Object.assign(device, apiObject);
|
||||||
|
device.controller = controller;
|
||||||
|
device.siteId = siteId || apiObject.site_id;
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the raw API object representation
|
||||||
|
*/
|
||||||
|
public toApiObject(): INetworkDevice {
|
||||||
|
return {
|
||||||
|
_id: this._id,
|
||||||
|
mac: this.mac,
|
||||||
|
model: this.model,
|
||||||
|
type: this.type,
|
||||||
|
name: this.name,
|
||||||
|
site_id: this.site_id,
|
||||||
|
adopted: this.adopted,
|
||||||
|
ip: this.ip,
|
||||||
|
state: this.state,
|
||||||
|
serial: this.serial,
|
||||||
|
version: this.version,
|
||||||
|
uptime: this.uptime,
|
||||||
|
last_seen: this.last_seen,
|
||||||
|
upgradable: this.upgradable,
|
||||||
|
upgrade_to_firmware: this.upgrade_to_firmware,
|
||||||
|
config_network: this.config_network,
|
||||||
|
ethernet_table: this.ethernet_table,
|
||||||
|
port_overrides: this.port_overrides,
|
||||||
|
sys_stats: this.sys_stats,
|
||||||
|
led_override: this.led_override,
|
||||||
|
led_override_color: this.led_override_color,
|
||||||
|
led_override_color_brightness: this.led_override_color_brightness,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if device is online (state 1 = connected)
|
||||||
|
*/
|
||||||
|
public isOnline(): boolean {
|
||||||
|
return this.state === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if device is an access point
|
||||||
|
*/
|
||||||
|
public isAccessPoint(): boolean {
|
||||||
|
return this.type === 'uap';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if device is a switch
|
||||||
|
*/
|
||||||
|
public isSwitch(): boolean {
|
||||||
|
return this.type === 'usw';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if device is a gateway/router
|
||||||
|
*/
|
||||||
|
public isGateway(): boolean {
|
||||||
|
return this.type === 'ugw' || this.type === 'udm';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if device has available firmware upgrade
|
||||||
|
*/
|
||||||
|
public hasUpgrade(): boolean {
|
||||||
|
return this.upgradable === true && !!this.upgrade_to_firmware;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get device display name (name or MAC if no name)
|
||||||
|
*/
|
||||||
|
public getDisplayName(): string {
|
||||||
|
return this.name || this.mac;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart the device
|
||||||
|
*/
|
||||||
|
public async restart(): Promise<void> {
|
||||||
|
if (!this.controller || !this.siteId) {
|
||||||
|
throw new Error('Cannot restart device: no controller reference');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.controller.request(
|
||||||
|
'POST',
|
||||||
|
`/api/s/${this.siteId}/cmd/devmgr`,
|
||||||
|
{
|
||||||
|
cmd: 'restart',
|
||||||
|
mac: this.mac,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrade the device firmware
|
||||||
|
*/
|
||||||
|
public async upgrade(): Promise<void> {
|
||||||
|
if (!this.controller || !this.siteId) {
|
||||||
|
throw new Error('Cannot upgrade device: no controller reference');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.controller.request(
|
||||||
|
'POST',
|
||||||
|
`/api/s/${this.siteId}/cmd/devmgr`,
|
||||||
|
{
|
||||||
|
cmd: 'upgrade',
|
||||||
|
mac: this.mac,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set LED override
|
||||||
|
*/
|
||||||
|
public async setLedOverride(mode: 'default' | 'on' | 'off'): Promise<void> {
|
||||||
|
if (!this.controller || !this.siteId) {
|
||||||
|
throw new Error('Cannot set LED: no controller reference');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.controller.request(
|
||||||
|
'PUT',
|
||||||
|
`/api/s/${this.siteId}/rest/device/${this._id}`,
|
||||||
|
{
|
||||||
|
led_override: mode,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename the device
|
||||||
|
*/
|
||||||
|
public async rename(newName: string): Promise<void> {
|
||||||
|
if (!this.controller || !this.siteId) {
|
||||||
|
throw new Error('Cannot rename device: no controller reference');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.controller.request(
|
||||||
|
'PUT',
|
||||||
|
`/api/s/${this.siteId}/rest/device/${this._id}`,
|
||||||
|
{
|
||||||
|
name: newName,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.name = newName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set port configuration
|
||||||
|
*/
|
||||||
|
public async setPortConfig(portIdx: number, config: Partial<IPortConfig>): Promise<void> {
|
||||||
|
if (!this.controller || !this.siteId) {
|
||||||
|
throw new Error('Cannot set port config: no controller reference');
|
||||||
|
}
|
||||||
|
|
||||||
|
const portOverride = {
|
||||||
|
port_idx: portIdx,
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get existing port overrides and update
|
||||||
|
const existingOverrides = this.port_overrides || [];
|
||||||
|
const overrideIndex = existingOverrides.findIndex((p) => p.port_idx === portIdx);
|
||||||
|
|
||||||
|
if (overrideIndex >= 0) {
|
||||||
|
existingOverrides[overrideIndex] = { ...existingOverrides[overrideIndex], ...portOverride };
|
||||||
|
} else {
|
||||||
|
existingOverrides.push(portOverride);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.controller.request(
|
||||||
|
'PUT',
|
||||||
|
`/api/s/${this.siteId}/rest/device/${this._id}`,
|
||||||
|
{
|
||||||
|
port_overrides: existingOverrides,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
188
ts/classes.devicemanager.ts
Normal file
188
ts/classes.devicemanager.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import type { UnifiController } from './classes.unifi-controller.js';
|
||||||
|
import { UnifiDevice } from './classes.device.js';
|
||||||
|
import type { INetworkDevice, IUnifiApiResponse } from './interfaces/index.js';
|
||||||
|
import { logger } from './unifi.logger.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager for UniFi network devices
|
||||||
|
*/
|
||||||
|
export class DeviceManager {
|
||||||
|
private controller: UnifiController;
|
||||||
|
|
||||||
|
constructor(controller: UnifiController) {
|
||||||
|
this.controller = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all devices for a site
|
||||||
|
*/
|
||||||
|
public async listDevices(siteId: string = 'default'): Promise<UnifiDevice[]> {
|
||||||
|
logger.log('debug', `Fetching devices for site: ${siteId}`);
|
||||||
|
|
||||||
|
const response = await this.controller.request<IUnifiApiResponse<INetworkDevice>>(
|
||||||
|
'GET',
|
||||||
|
`/api/s/${siteId}/stat/device`
|
||||||
|
);
|
||||||
|
|
||||||
|
const devices: UnifiDevice[] = [];
|
||||||
|
for (const deviceData of response.data || []) {
|
||||||
|
devices.push(UnifiDevice.createFromApiObject(deviceData, this.controller, siteId));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', `Found ${devices.length} devices`);
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a device by MAC address
|
||||||
|
*/
|
||||||
|
public async getDeviceByMac(mac: string, siteId: string = 'default'): Promise<UnifiDevice | null> {
|
||||||
|
const normalizedMac = mac.toLowerCase().replace(/[:-]/g, ':');
|
||||||
|
const devices = await this.listDevices(siteId);
|
||||||
|
return devices.find((device) => device.mac.toLowerCase() === normalizedMac) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a device by ID
|
||||||
|
*/
|
||||||
|
public async getDeviceById(deviceId: string, siteId: string = 'default'): Promise<UnifiDevice | null> {
|
||||||
|
const devices = await this.listDevices(siteId);
|
||||||
|
return devices.find((device) => device._id === deviceId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get access points only
|
||||||
|
*/
|
||||||
|
public async getAccessPoints(siteId: string = 'default'): Promise<UnifiDevice[]> {
|
||||||
|
const devices = await this.listDevices(siteId);
|
||||||
|
return devices.filter((device) => device.isAccessPoint());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get switches only
|
||||||
|
*/
|
||||||
|
public async getSwitches(siteId: string = 'default'): Promise<UnifiDevice[]> {
|
||||||
|
const devices = await this.listDevices(siteId);
|
||||||
|
return devices.filter((device) => device.isSwitch());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get gateways only
|
||||||
|
*/
|
||||||
|
public async getGateways(siteId: string = 'default'): Promise<UnifiDevice[]> {
|
||||||
|
const devices = await this.listDevices(siteId);
|
||||||
|
return devices.filter((device) => device.isGateway());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get devices with available upgrades
|
||||||
|
*/
|
||||||
|
public async getUpgradableDevices(siteId: string = 'default'): Promise<UnifiDevice[]> {
|
||||||
|
const devices = await this.listDevices(siteId);
|
||||||
|
return devices.filter((device) => device.hasUpgrade());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get offline devices
|
||||||
|
*/
|
||||||
|
public async getOfflineDevices(siteId: string = 'default'): Promise<UnifiDevice[]> {
|
||||||
|
const devices = await this.listDevices(siteId);
|
||||||
|
return devices.filter((device) => !device.isOnline());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart a device by MAC
|
||||||
|
*/
|
||||||
|
public async restartDevice(mac: string, siteId: string = 'default'): Promise<void> {
|
||||||
|
logger.log('info', `Restarting device: ${mac}`);
|
||||||
|
|
||||||
|
await this.controller.request(
|
||||||
|
'POST',
|
||||||
|
`/api/s/${siteId}/cmd/devmgr`,
|
||||||
|
{
|
||||||
|
cmd: 'restart',
|
||||||
|
mac: mac.toLowerCase(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrade a device's firmware
|
||||||
|
*/
|
||||||
|
public async upgradeDevice(mac: string, siteId: string = 'default'): Promise<void> {
|
||||||
|
logger.log('info', `Upgrading device: ${mac}`);
|
||||||
|
|
||||||
|
await this.controller.request(
|
||||||
|
'POST',
|
||||||
|
`/api/s/${siteId}/cmd/devmgr`,
|
||||||
|
{
|
||||||
|
cmd: 'upgrade',
|
||||||
|
mac: mac.toLowerCase(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adopt a device
|
||||||
|
*/
|
||||||
|
public async adoptDevice(mac: string, siteId: string = 'default'): Promise<void> {
|
||||||
|
logger.log('info', `Adopting device: ${mac}`);
|
||||||
|
|
||||||
|
await this.controller.request(
|
||||||
|
'POST',
|
||||||
|
`/api/s/${siteId}/cmd/devmgr`,
|
||||||
|
{
|
||||||
|
cmd: 'adopt',
|
||||||
|
mac: mac.toLowerCase(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forget a device (remove from controller)
|
||||||
|
*/
|
||||||
|
public async forgetDevice(mac: string, siteId: string = 'default'): Promise<void> {
|
||||||
|
logger.log('info', `Forgetting device: ${mac}`);
|
||||||
|
|
||||||
|
await this.controller.request(
|
||||||
|
'POST',
|
||||||
|
`/api/s/${siteId}/cmd/devmgr`,
|
||||||
|
{
|
||||||
|
cmd: 'delete-device',
|
||||||
|
mac: mac.toLowerCase(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locate device (flash LEDs)
|
||||||
|
*/
|
||||||
|
public async locateDevice(mac: string, enabled: boolean, siteId: string = 'default'): Promise<void> {
|
||||||
|
logger.log('info', `${enabled ? 'Locating' : 'Stop locating'} device: ${mac}`);
|
||||||
|
|
||||||
|
await this.controller.request(
|
||||||
|
'POST',
|
||||||
|
`/api/s/${siteId}/cmd/devmgr`,
|
||||||
|
{
|
||||||
|
cmd: enabled ? 'set-locate' : 'unset-locate',
|
||||||
|
mac: mac.toLowerCase(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force provision a device (push config)
|
||||||
|
*/
|
||||||
|
public async provisionDevice(mac: string, siteId: string = 'default'): Promise<void> {
|
||||||
|
logger.log('info', `Provisioning device: ${mac}`);
|
||||||
|
|
||||||
|
await this.controller.request(
|
||||||
|
'POST',
|
||||||
|
`/api/s/${siteId}/cmd/devmgr`,
|
||||||
|
{
|
||||||
|
cmd: 'force-provision',
|
||||||
|
mac: mac.toLowerCase(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
155
ts/classes.door.ts
Normal file
155
ts/classes.door.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import type { UnifiAccess } from './classes.unifi-access.js';
|
||||||
|
import type { IAccessDoor, IAccessDoorRule, IAccessFloor } from './interfaces/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a UniFi Access door
|
||||||
|
*/
|
||||||
|
export class UnifiDoor implements IAccessDoor {
|
||||||
|
/** Reference to parent access instance */
|
||||||
|
private access?: UnifiAccess;
|
||||||
|
|
||||||
|
// IAccessDoor properties
|
||||||
|
public unique_id: string;
|
||||||
|
public name: string;
|
||||||
|
public alias?: string;
|
||||||
|
public door_type?: string;
|
||||||
|
public door_lock_relay_status?: 'lock' | 'unlock';
|
||||||
|
public door_position_status?: 'open' | 'close';
|
||||||
|
public device_id?: string;
|
||||||
|
public camera_resource_id?: string;
|
||||||
|
public location_id?: string;
|
||||||
|
public full_name?: string;
|
||||||
|
public extra_type?: string;
|
||||||
|
public door_guard?: boolean;
|
||||||
|
public rules?: IAccessDoorRule[];
|
||||||
|
public level_id?: string;
|
||||||
|
public floor?: IAccessFloor;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.unique_id = '';
|
||||||
|
this.name = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a door instance from API response object
|
||||||
|
*/
|
||||||
|
public static createFromApiObject(
|
||||||
|
apiObject: IAccessDoor,
|
||||||
|
access?: UnifiAccess
|
||||||
|
): UnifiDoor {
|
||||||
|
const door = new UnifiDoor();
|
||||||
|
Object.assign(door, apiObject);
|
||||||
|
door.access = access;
|
||||||
|
return door;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the raw API object representation
|
||||||
|
*/
|
||||||
|
public toApiObject(): IAccessDoor {
|
||||||
|
return {
|
||||||
|
unique_id: this.unique_id,
|
||||||
|
name: this.name,
|
||||||
|
alias: this.alias,
|
||||||
|
door_type: this.door_type,
|
||||||
|
door_lock_relay_status: this.door_lock_relay_status,
|
||||||
|
door_position_status: this.door_position_status,
|
||||||
|
device_id: this.device_id,
|
||||||
|
camera_resource_id: this.camera_resource_id,
|
||||||
|
location_id: this.location_id,
|
||||||
|
full_name: this.full_name,
|
||||||
|
extra_type: this.extra_type,
|
||||||
|
door_guard: this.door_guard,
|
||||||
|
rules: this.rules,
|
||||||
|
level_id: this.level_id,
|
||||||
|
floor: this.floor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get display name (alias or name)
|
||||||
|
*/
|
||||||
|
public getDisplayName(): string {
|
||||||
|
return this.alias || this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if door is currently locked
|
||||||
|
*/
|
||||||
|
public isLocked(): boolean {
|
||||||
|
return this.door_lock_relay_status === 'lock';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if door is currently open (contact sensor)
|
||||||
|
*/
|
||||||
|
public isOpen(): boolean {
|
||||||
|
return this.door_position_status === 'open';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if door is currently closed
|
||||||
|
*/
|
||||||
|
public isClosed(): boolean {
|
||||||
|
return this.door_position_status === 'close';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlock the door
|
||||||
|
*/
|
||||||
|
public async unlock(): Promise<void> {
|
||||||
|
if (!this.access) {
|
||||||
|
throw new Error('Cannot unlock door: no access reference');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.access.unlockDoor(this.unique_id);
|
||||||
|
this.door_lock_relay_status = 'unlock';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock the door
|
||||||
|
*/
|
||||||
|
public async lock(): Promise<void> {
|
||||||
|
if (!this.access) {
|
||||||
|
throw new Error('Cannot lock door: no access reference');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.access.lockDoor(this.unique_id);
|
||||||
|
this.door_lock_relay_status = 'lock';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update door settings
|
||||||
|
*/
|
||||||
|
public async updateSettings(settings: Partial<IAccessDoor>): Promise<void> {
|
||||||
|
if (!this.access) {
|
||||||
|
throw new Error('Cannot update door: no access reference');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.access.request('PUT', `/door/${this.unique_id}`, settings);
|
||||||
|
Object.assign(this, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename the door
|
||||||
|
*/
|
||||||
|
public async rename(newName: string): Promise<void> {
|
||||||
|
await this.updateSettings({ name: newName });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set door alias
|
||||||
|
*/
|
||||||
|
public async setAlias(alias: string): Promise<void> {
|
||||||
|
await this.updateSettings({ alias });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get door status summary
|
||||||
|
*/
|
||||||
|
public getStatus(): string {
|
||||||
|
const lockStatus = this.isLocked() ? 'Locked' : 'Unlocked';
|
||||||
|
const positionStatus = this.isOpen() ? 'Open' : 'Closed';
|
||||||
|
return `${lockStatus}, ${positionStatus}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
168
ts/classes.doormanager.ts
Normal file
168
ts/classes.doormanager.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import type { UnifiAccess } from './classes.unifi-access.js';
|
||||||
|
import { UnifiDoor } from './classes.door.js';
|
||||||
|
import type { IAccessDoor, IAccessApiResponse, IAccessEvent } from './interfaces/index.js';
|
||||||
|
import { logger } from './unifi.logger.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager for UniFi Access doors
|
||||||
|
*/
|
||||||
|
export class DoorManager {
|
||||||
|
private access: UnifiAccess;
|
||||||
|
|
||||||
|
constructor(access: UnifiAccess) {
|
||||||
|
this.access = access;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all doors
|
||||||
|
*/
|
||||||
|
public async listDoors(): Promise<UnifiDoor[]> {
|
||||||
|
logger.log('debug', 'Fetching doors from Access');
|
||||||
|
|
||||||
|
const response = await this.access.request<IAccessApiResponse<IAccessDoor[]>>(
|
||||||
|
'GET',
|
||||||
|
'/door'
|
||||||
|
);
|
||||||
|
|
||||||
|
const doors: UnifiDoor[] = [];
|
||||||
|
for (const doorData of response.data || []) {
|
||||||
|
doors.push(UnifiDoor.createFromApiObject(doorData, this.access));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', `Found ${doors.length} doors`);
|
||||||
|
return doors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a door by ID
|
||||||
|
*/
|
||||||
|
public async getDoorById(doorId: string): Promise<UnifiDoor | null> {
|
||||||
|
logger.log('debug', `Fetching door: ${doorId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.access.request<IAccessApiResponse<IAccessDoor>>(
|
||||||
|
'GET',
|
||||||
|
`/door/${doorId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return UnifiDoor.createFromApiObject(response.data, this.access);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log('warn', `Door not found: ${doorId}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a door by name
|
||||||
|
*/
|
||||||
|
public async getDoorByName(name: string): Promise<UnifiDoor | null> {
|
||||||
|
const doors = await this.listDoors();
|
||||||
|
return doors.find(
|
||||||
|
(door) =>
|
||||||
|
door.name.toLowerCase() === name.toLowerCase() ||
|
||||||
|
door.alias?.toLowerCase() === name.toLowerCase()
|
||||||
|
) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get locked doors only
|
||||||
|
*/
|
||||||
|
public async getLockedDoors(): Promise<UnifiDoor[]> {
|
||||||
|
const doors = await this.listDoors();
|
||||||
|
return doors.filter((door) => door.isLocked());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get unlocked doors only
|
||||||
|
*/
|
||||||
|
public async getUnlockedDoors(): Promise<UnifiDoor[]> {
|
||||||
|
const doors = await this.listDoors();
|
||||||
|
return doors.filter((door) => !door.isLocked());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get open doors only
|
||||||
|
*/
|
||||||
|
public async getOpenDoors(): Promise<UnifiDoor[]> {
|
||||||
|
const doors = await this.listDoors();
|
||||||
|
return doors.filter((door) => door.isOpen());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get doors by location
|
||||||
|
*/
|
||||||
|
public async getDoorsByLocation(locationId: string): Promise<UnifiDoor[]> {
|
||||||
|
const doors = await this.listDoors();
|
||||||
|
return doors.filter((door) => door.location_id === locationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlock a door by ID
|
||||||
|
*/
|
||||||
|
public async unlockDoor(doorId: string): Promise<void> {
|
||||||
|
logger.log('info', `Unlocking door: ${doorId}`);
|
||||||
|
|
||||||
|
await this.access.request('PUT', `/door/${doorId}/unlock`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock a door by ID
|
||||||
|
*/
|
||||||
|
public async lockDoor(doorId: string): Promise<void> {
|
||||||
|
logger.log('info', `Locking door: ${doorId}`);
|
||||||
|
|
||||||
|
await this.access.request('PUT', `/door/${doorId}/lock`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlock all doors
|
||||||
|
*/
|
||||||
|
public async unlockAllDoors(): Promise<void> {
|
||||||
|
logger.log('info', 'Unlocking all doors');
|
||||||
|
|
||||||
|
const doors = await this.listDoors();
|
||||||
|
await Promise.all(doors.map((door) => this.unlockDoor(door.unique_id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock all doors
|
||||||
|
*/
|
||||||
|
public async lockAllDoors(): Promise<void> {
|
||||||
|
logger.log('info', 'Locking all doors');
|
||||||
|
|
||||||
|
const doors = await this.listDoors();
|
||||||
|
await Promise.all(doors.map((door) => this.lockDoor(door.unique_id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get access events for a door
|
||||||
|
*/
|
||||||
|
public async getDoorEvents(
|
||||||
|
doorId: string,
|
||||||
|
options: { start?: number; end?: number; limit?: number } = {}
|
||||||
|
): Promise<IAccessEvent[]> {
|
||||||
|
logger.log('debug', `Fetching events for door: ${doorId}`);
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('door_id', doorId);
|
||||||
|
if (options.start) params.append('start', options.start.toString());
|
||||||
|
if (options.end) params.append('end', options.end.toString());
|
||||||
|
if (options.limit) params.append('limit', options.limit.toString());
|
||||||
|
|
||||||
|
const response = await this.access.request<IAccessApiResponse<IAccessEvent[]>>(
|
||||||
|
'GET',
|
||||||
|
`/event?${params.toString()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update door settings
|
||||||
|
*/
|
||||||
|
public async updateDoor(doorId: string, settings: Partial<IAccessDoor>): Promise<void> {
|
||||||
|
logger.log('info', `Updating door: ${doorId}`);
|
||||||
|
|
||||||
|
await this.access.request('PUT', `/door/${doorId}`, settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
85
ts/classes.host.ts
Normal file
85
ts/classes.host.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import type { UnifiAccount } from './classes.unifi-account.js';
|
||||||
|
import type { IUnifiHost } from './interfaces/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a UniFi host device from Site Manager
|
||||||
|
*/
|
||||||
|
export class UnifiHost implements IUnifiHost {
|
||||||
|
/** Reference to parent account */
|
||||||
|
private unifiAccount?: UnifiAccount;
|
||||||
|
|
||||||
|
// IUnifiHost properties
|
||||||
|
public id: string;
|
||||||
|
public hardwareId?: string;
|
||||||
|
public name?: string;
|
||||||
|
public type?: string;
|
||||||
|
public firmwareVersion?: string;
|
||||||
|
public isOnline?: boolean;
|
||||||
|
public ipAddress?: string;
|
||||||
|
public macAddress?: string;
|
||||||
|
public siteId?: string;
|
||||||
|
public status?: {
|
||||||
|
state?: string;
|
||||||
|
lastSeen?: string;
|
||||||
|
};
|
||||||
|
public features?: string[];
|
||||||
|
public reportedState?: Record<string, unknown>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.id = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a host instance from API response object
|
||||||
|
*/
|
||||||
|
public static createFromApiObject(
|
||||||
|
apiObject: IUnifiHost,
|
||||||
|
unifiAccount?: UnifiAccount
|
||||||
|
): UnifiHost {
|
||||||
|
const host = new UnifiHost();
|
||||||
|
Object.assign(host, apiObject);
|
||||||
|
host.unifiAccount = unifiAccount;
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the raw API object representation
|
||||||
|
*/
|
||||||
|
public toApiObject(): IUnifiHost {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
hardwareId: this.hardwareId,
|
||||||
|
name: this.name,
|
||||||
|
type: this.type,
|
||||||
|
firmwareVersion: this.firmwareVersion,
|
||||||
|
isOnline: this.isOnline,
|
||||||
|
ipAddress: this.ipAddress,
|
||||||
|
macAddress: this.macAddress,
|
||||||
|
siteId: this.siteId,
|
||||||
|
status: this.status,
|
||||||
|
features: this.features,
|
||||||
|
reportedState: this.reportedState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if host is online
|
||||||
|
*/
|
||||||
|
public checkOnline(): boolean {
|
||||||
|
return this.isOnline === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get host type (e.g., 'udm-pro', 'cloud-key')
|
||||||
|
*/
|
||||||
|
public getType(): string {
|
||||||
|
return this.type || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if host supports a specific feature
|
||||||
|
*/
|
||||||
|
public hasFeature(feature: string): boolean {
|
||||||
|
return this.features?.includes(feature) ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
ts/classes.hostmanager.ts
Normal file
78
ts/classes.hostmanager.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import type { UnifiAccount } from './classes.unifi-account.js';
|
||||||
|
import { UnifiHost } from './classes.host.js';
|
||||||
|
import type { IUnifiHost, ISiteManagerListResponse } from './interfaces/index.js';
|
||||||
|
import { logger } from './unifi.logger.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager for UniFi hosts via Site Manager API
|
||||||
|
*/
|
||||||
|
export class HostManager {
|
||||||
|
private unifiAccount: UnifiAccount;
|
||||||
|
|
||||||
|
constructor(unifiAccount: UnifiAccount) {
|
||||||
|
this.unifiAccount = unifiAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all hosts
|
||||||
|
*/
|
||||||
|
public async listHosts(): Promise<UnifiHost[]> {
|
||||||
|
logger.log('debug', 'Fetching all hosts from Site Manager');
|
||||||
|
|
||||||
|
const response = await this.unifiAccount.request<ISiteManagerListResponse<IUnifiHost>>(
|
||||||
|
'GET',
|
||||||
|
'/ea/hosts'
|
||||||
|
);
|
||||||
|
|
||||||
|
const hosts: UnifiHost[] = [];
|
||||||
|
for (const hostData of response.data || []) {
|
||||||
|
hosts.push(UnifiHost.createFromApiObject(hostData, this.unifiAccount));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', `Found ${hosts.length} hosts`);
|
||||||
|
return hosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a host by ID
|
||||||
|
*/
|
||||||
|
public async getHostById(hostId: string): Promise<UnifiHost | null> {
|
||||||
|
logger.log('debug', `Fetching host: ${hostId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.unifiAccount.request<IUnifiHost>(
|
||||||
|
'GET',
|
||||||
|
`/ea/hosts/${hostId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return UnifiHost.createFromApiObject(response, this.unifiAccount);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log('warn', `Host not found: ${hostId}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a host by name
|
||||||
|
*/
|
||||||
|
public async findHostByName(name: string): Promise<UnifiHost | null> {
|
||||||
|
const hosts = await this.listHosts();
|
||||||
|
return hosts.find((host) => host.name === name) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get hosts by site ID
|
||||||
|
*/
|
||||||
|
public async getHostsBySiteId(siteId: string): Promise<UnifiHost[]> {
|
||||||
|
const hosts = await this.listHosts();
|
||||||
|
return hosts.filter((host) => host.siteId === siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get online hosts only
|
||||||
|
*/
|
||||||
|
public async getOnlineHosts(): Promise<UnifiHost[]> {
|
||||||
|
const hosts = await this.listHosts();
|
||||||
|
return hosts.filter((host) => host.checkOnline());
|
||||||
|
}
|
||||||
|
}
|
||||||
57
ts/classes.site.ts
Normal file
57
ts/classes.site.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import type { UnifiAccount } from './classes.unifi-account.js';
|
||||||
|
import type { IUnifiSite } from './interfaces/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a UniFi site from Site Manager
|
||||||
|
*/
|
||||||
|
export class UnifiSite implements IUnifiSite {
|
||||||
|
/** Reference to parent account */
|
||||||
|
private unifiAccount?: UnifiAccount;
|
||||||
|
|
||||||
|
// IUnifiSite properties
|
||||||
|
public siteId: string;
|
||||||
|
public name: string;
|
||||||
|
public description?: string;
|
||||||
|
public isDefault?: boolean;
|
||||||
|
public timezone?: string;
|
||||||
|
public meta?: {
|
||||||
|
type?: string;
|
||||||
|
address?: string;
|
||||||
|
};
|
||||||
|
public createdAt?: string;
|
||||||
|
public updatedAt?: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.siteId = '';
|
||||||
|
this.name = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a site instance from API response object
|
||||||
|
*/
|
||||||
|
public static createFromApiObject(
|
||||||
|
apiObject: IUnifiSite,
|
||||||
|
unifiAccount?: UnifiAccount
|
||||||
|
): UnifiSite {
|
||||||
|
const site = new UnifiSite();
|
||||||
|
Object.assign(site, apiObject);
|
||||||
|
site.unifiAccount = unifiAccount;
|
||||||
|
return site;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the raw API object representation
|
||||||
|
*/
|
||||||
|
public toApiObject(): IUnifiSite {
|
||||||
|
return {
|
||||||
|
siteId: this.siteId,
|
||||||
|
name: this.name,
|
||||||
|
description: this.description,
|
||||||
|
isDefault: this.isDefault,
|
||||||
|
timezone: this.timezone,
|
||||||
|
meta: this.meta,
|
||||||
|
createdAt: this.createdAt,
|
||||||
|
updatedAt: this.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
62
ts/classes.sitemanager.ts
Normal file
62
ts/classes.sitemanager.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import type { UnifiAccount } from './classes.unifi-account.js';
|
||||||
|
import { UnifiSite } from './classes.site.js';
|
||||||
|
import type { IUnifiSite, ISiteManagerListResponse } from './interfaces/index.js';
|
||||||
|
import { logger } from './unifi.logger.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager for UniFi sites via Site Manager API
|
||||||
|
*/
|
||||||
|
export class SiteManager {
|
||||||
|
private unifiAccount: UnifiAccount;
|
||||||
|
|
||||||
|
constructor(unifiAccount: UnifiAccount) {
|
||||||
|
this.unifiAccount = unifiAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all sites
|
||||||
|
*/
|
||||||
|
public async listSites(): Promise<UnifiSite[]> {
|
||||||
|
logger.log('debug', 'Fetching all sites from Site Manager');
|
||||||
|
|
||||||
|
const response = await this.unifiAccount.request<ISiteManagerListResponse<IUnifiSite>>(
|
||||||
|
'GET',
|
||||||
|
'/ea/sites'
|
||||||
|
);
|
||||||
|
|
||||||
|
const sites: UnifiSite[] = [];
|
||||||
|
for (const siteData of response.data || []) {
|
||||||
|
sites.push(UnifiSite.createFromApiObject(siteData, this.unifiAccount));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', `Found ${sites.length} sites`);
|
||||||
|
return sites;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a site by ID
|
||||||
|
*/
|
||||||
|
public async getSiteById(siteId: string): Promise<UnifiSite | null> {
|
||||||
|
logger.log('debug', `Fetching site: ${siteId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.unifiAccount.request<IUnifiSite>(
|
||||||
|
'GET',
|
||||||
|
`/ea/sites/${siteId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return UnifiSite.createFromApiObject(response, this.unifiAccount);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log('warn', `Site not found: ${siteId}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a site by name
|
||||||
|
*/
|
||||||
|
public async findSiteByName(name: string): Promise<UnifiSite | null> {
|
||||||
|
const sites = await this.listSites();
|
||||||
|
return sites.find((site) => site.name === name) || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
339
ts/classes.unifi-access.ts
Normal file
339
ts/classes.unifi-access.ts
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import { logger } from './unifi.logger.js';
|
||||||
|
import { UnifiHttp } from './classes.unifihttp.js';
|
||||||
|
import { DoorManager } from './classes.doormanager.js';
|
||||||
|
import type {
|
||||||
|
IUnifiAccessOptions,
|
||||||
|
IAccessDevice,
|
||||||
|
IAccessUser,
|
||||||
|
IAccessPolicy,
|
||||||
|
IAccessLocation,
|
||||||
|
IAccessEvent,
|
||||||
|
IAccessApiResponse,
|
||||||
|
THttpMethod,
|
||||||
|
} from './interfaces/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UniFi Access - Entry point for Access Controller API
|
||||||
|
*
|
||||||
|
* This class provides access to the UniFi Access API for managing doors,
|
||||||
|
* users, credentials, and access events. It uses bearer token authentication.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const access = new UnifiAccess({
|
||||||
|
* host: '192.168.1.1',
|
||||||
|
* token: 'your-bearer-token',
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* const doors = await access.doorManager.listDoors();
|
||||||
|
* const users = await access.getUsers();
|
||||||
|
*
|
||||||
|
* // Unlock a door
|
||||||
|
* await access.unlockDoor('door-id');
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class UnifiAccess {
|
||||||
|
/** Access API port */
|
||||||
|
private static readonly API_PORT = 12445;
|
||||||
|
|
||||||
|
/** Access host */
|
||||||
|
private host: string;
|
||||||
|
|
||||||
|
/** Bearer token for authentication */
|
||||||
|
private token: string;
|
||||||
|
|
||||||
|
/** Whether to verify SSL certificates */
|
||||||
|
private verifySsl: boolean;
|
||||||
|
|
||||||
|
/** HTTP client */
|
||||||
|
private http: UnifiHttp;
|
||||||
|
|
||||||
|
/** Door manager instance */
|
||||||
|
public doorManager: DoorManager;
|
||||||
|
|
||||||
|
constructor(options: IUnifiAccessOptions) {
|
||||||
|
this.host = options.host.replace(/\/$/, '');
|
||||||
|
this.token = options.token;
|
||||||
|
this.verifySsl = options.verifySsl ?? false;
|
||||||
|
|
||||||
|
// Build base URL with Access API port
|
||||||
|
const baseHost = this.host.startsWith('http') ? this.host : `https://${this.host}`;
|
||||||
|
// Access API is typically on port 12445 at /api/v1/developer
|
||||||
|
const baseUrl = `${baseHost}:${UnifiAccess.API_PORT}/api/v1/developer`;
|
||||||
|
|
||||||
|
this.http = new UnifiHttp(baseUrl, this.verifySsl);
|
||||||
|
this.http.setHeader('Authorization', `Bearer ${this.token}`);
|
||||||
|
|
||||||
|
// Initialize managers
|
||||||
|
this.doorManager = new DoorManager(this);
|
||||||
|
|
||||||
|
logger.log('info', `UnifiAccess initialized for ${this.host}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a request to the Access API
|
||||||
|
*/
|
||||||
|
public async request<T>(
|
||||||
|
method: THttpMethod,
|
||||||
|
endpoint: string,
|
||||||
|
data?: unknown
|
||||||
|
): Promise<T> {
|
||||||
|
return this.http.request<T>(method, endpoint, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlock a door by ID
|
||||||
|
*/
|
||||||
|
public async unlockDoor(doorId: string): Promise<void> {
|
||||||
|
logger.log('info', `Unlocking door: ${doorId}`);
|
||||||
|
|
||||||
|
await this.request('PUT', `/door/${doorId}/unlock`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock a door by ID
|
||||||
|
*/
|
||||||
|
public async lockDoor(doorId: string): Promise<void> {
|
||||||
|
logger.log('info', `Locking door: ${doorId}`);
|
||||||
|
|
||||||
|
await this.request('PUT', `/door/${doorId}/lock`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all doors (convenience method)
|
||||||
|
*/
|
||||||
|
public async getDoors() {
|
||||||
|
return this.doorManager.listDoors();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all devices (hubs, readers, etc.)
|
||||||
|
*/
|
||||||
|
public async getDevices(): Promise<IAccessDevice[]> {
|
||||||
|
logger.log('debug', 'Fetching Access devices');
|
||||||
|
|
||||||
|
const response = await this.request<IAccessApiResponse<IAccessDevice[]>>(
|
||||||
|
'GET',
|
||||||
|
'/device'
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a device by ID
|
||||||
|
*/
|
||||||
|
public async getDeviceById(deviceId: string): Promise<IAccessDevice | null> {
|
||||||
|
try {
|
||||||
|
const response = await this.request<IAccessApiResponse<IAccessDevice>>(
|
||||||
|
'GET',
|
||||||
|
`/device/${deviceId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all users/credential holders
|
||||||
|
*/
|
||||||
|
public async getUsers(): Promise<IAccessUser[]> {
|
||||||
|
logger.log('debug', 'Fetching Access users');
|
||||||
|
|
||||||
|
const response = await this.request<IAccessApiResponse<IAccessUser[]>>(
|
||||||
|
'GET',
|
||||||
|
'/user'
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a user by ID
|
||||||
|
*/
|
||||||
|
public async getUserById(userId: string): Promise<IAccessUser | null> {
|
||||||
|
try {
|
||||||
|
const response = await this.request<IAccessApiResponse<IAccessUser>>(
|
||||||
|
'GET',
|
||||||
|
`/user/${userId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user
|
||||||
|
*/
|
||||||
|
public async createUser(user: Partial<IAccessUser>): Promise<IAccessUser> {
|
||||||
|
logger.log('info', `Creating user: ${user.first_name} ${user.last_name}`);
|
||||||
|
|
||||||
|
const response = await this.request<IAccessApiResponse<IAccessUser>>(
|
||||||
|
'POST',
|
||||||
|
'/user',
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a user
|
||||||
|
*/
|
||||||
|
public async updateUser(userId: string, user: Partial<IAccessUser>): Promise<IAccessUser> {
|
||||||
|
logger.log('info', `Updating user: ${userId}`);
|
||||||
|
|
||||||
|
const response = await this.request<IAccessApiResponse<IAccessUser>>(
|
||||||
|
'PUT',
|
||||||
|
`/user/${userId}`,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a user
|
||||||
|
*/
|
||||||
|
public async deleteUser(userId: string): Promise<void> {
|
||||||
|
logger.log('info', `Deleting user: ${userId}`);
|
||||||
|
|
||||||
|
await this.request('DELETE', `/user/${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get access policies/groups
|
||||||
|
*/
|
||||||
|
public async getPolicies(): Promise<IAccessPolicy[]> {
|
||||||
|
logger.log('debug', 'Fetching Access policies');
|
||||||
|
|
||||||
|
const response = await this.request<IAccessApiResponse<IAccessPolicy[]>>(
|
||||||
|
'GET',
|
||||||
|
'/policy'
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a policy by ID
|
||||||
|
*/
|
||||||
|
public async getPolicyById(policyId: string): Promise<IAccessPolicy | null> {
|
||||||
|
try {
|
||||||
|
const response = await this.request<IAccessApiResponse<IAccessPolicy>>(
|
||||||
|
'GET',
|
||||||
|
`/policy/${policyId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get locations
|
||||||
|
*/
|
||||||
|
public async getLocations(): Promise<IAccessLocation[]> {
|
||||||
|
logger.log('debug', 'Fetching Access locations');
|
||||||
|
|
||||||
|
const response = await this.request<IAccessApiResponse<IAccessLocation[]>>(
|
||||||
|
'GET',
|
||||||
|
'/location'
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get access events (entry log)
|
||||||
|
*/
|
||||||
|
public async getEvents(
|
||||||
|
options: { start?: number; end?: number; limit?: number; doorId?: string; userId?: string } = {}
|
||||||
|
): Promise<IAccessEvent[]> {
|
||||||
|
logger.log('debug', 'Fetching Access events');
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (options.start) params.append('start', options.start.toString());
|
||||||
|
if (options.end) params.append('end', options.end.toString());
|
||||||
|
if (options.limit) params.append('limit', options.limit.toString());
|
||||||
|
if (options.doorId) params.append('door_id', options.doorId);
|
||||||
|
if (options.userId) params.append('user_id', options.userId);
|
||||||
|
|
||||||
|
const queryString = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
const response = await this.request<IAccessApiResponse<IAccessEvent[]>>(
|
||||||
|
'GET',
|
||||||
|
`/event${queryString}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recent access events
|
||||||
|
*/
|
||||||
|
public async getRecentEvents(limit: number = 100): Promise<IAccessEvent[]> {
|
||||||
|
return this.getEvents({ limit });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grant user access to a door
|
||||||
|
*/
|
||||||
|
public async grantAccess(userId: string, doorId: string): Promise<void> {
|
||||||
|
logger.log('info', `Granting user ${userId} access to door ${doorId}`);
|
||||||
|
|
||||||
|
await this.request('POST', `/user/${userId}/access`, {
|
||||||
|
door_id: doorId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke user access from a door
|
||||||
|
*/
|
||||||
|
public async revokeAccess(userId: string, doorId: string): Promise<void> {
|
||||||
|
logger.log('info', `Revoking user ${userId} access from door ${doorId}`);
|
||||||
|
|
||||||
|
await this.request('DELETE', `/user/${userId}/access/${doorId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign NFC card to user
|
||||||
|
*/
|
||||||
|
public async assignNfcCard(
|
||||||
|
userId: string,
|
||||||
|
cardToken: string,
|
||||||
|
alias?: string
|
||||||
|
): Promise<void> {
|
||||||
|
logger.log('info', `Assigning NFC card to user ${userId}`);
|
||||||
|
|
||||||
|
await this.request('POST', `/user/${userId}/nfc_card`, {
|
||||||
|
token: cardToken,
|
||||||
|
alias: alias,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove NFC card from user
|
||||||
|
*/
|
||||||
|
public async removeNfcCard(userId: string, cardId: string): Promise<void> {
|
||||||
|
logger.log('info', `Removing NFC card ${cardId} from user ${userId}`);
|
||||||
|
|
||||||
|
await this.request('DELETE', `/user/${userId}/nfc_card/${cardId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set user PIN code
|
||||||
|
*/
|
||||||
|
public async setUserPin(userId: string, pinCode: string): Promise<void> {
|
||||||
|
logger.log('info', `Setting PIN for user ${userId}`);
|
||||||
|
|
||||||
|
await this.request('PUT', `/user/${userId}`, {
|
||||||
|
pin_code: pinCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
82
ts/classes.unifi-account.ts
Normal file
82
ts/classes.unifi-account.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import { logger } from './unifi.logger.js';
|
||||||
|
import { UnifiHttp } from './classes.unifihttp.js';
|
||||||
|
import { SiteManager } from './classes.sitemanager.js';
|
||||||
|
import { HostManager } from './classes.hostmanager.js';
|
||||||
|
import type { IUnifiAccountOptions, THttpMethod } from './interfaces/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UniFi Account - Entry point for Site Manager (cloud) API
|
||||||
|
*
|
||||||
|
* This class provides access to the UniFi Site Manager API using API key authentication.
|
||||||
|
* It's used to manage sites and hosts across your UniFi deployment via ui.com.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const account = new UnifiAccount({ apiKey: 'your-api-key' });
|
||||||
|
* const sites = await account.siteManager.listSites();
|
||||||
|
* const hosts = await account.hostManager.listHosts();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class UnifiAccount {
|
||||||
|
/** Site Manager API base URL */
|
||||||
|
private static readonly BASE_URL = 'https://api.ui.com/v1';
|
||||||
|
|
||||||
|
/** API key for authentication */
|
||||||
|
private apiKey: string;
|
||||||
|
|
||||||
|
/** HTTP client */
|
||||||
|
private http: UnifiHttp;
|
||||||
|
|
||||||
|
/** Site manager instance */
|
||||||
|
public siteManager: SiteManager;
|
||||||
|
|
||||||
|
/** Host manager instance */
|
||||||
|
public hostManager: HostManager;
|
||||||
|
|
||||||
|
constructor(options: IUnifiAccountOptions) {
|
||||||
|
this.apiKey = options.apiKey;
|
||||||
|
|
||||||
|
// Initialize HTTP client
|
||||||
|
this.http = new UnifiHttp(UnifiAccount.BASE_URL, true); // Cloud API uses valid SSL
|
||||||
|
this.http.setHeader('X-API-Key', this.apiKey);
|
||||||
|
|
||||||
|
// Initialize managers
|
||||||
|
this.siteManager = new SiteManager(this);
|
||||||
|
this.hostManager = new HostManager(this);
|
||||||
|
|
||||||
|
logger.log('info', 'UnifiAccount initialized for Site Manager API');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a request to the Site Manager API
|
||||||
|
*/
|
||||||
|
public async request<T>(
|
||||||
|
method: THttpMethod,
|
||||||
|
endpoint: string,
|
||||||
|
data?: unknown
|
||||||
|
): Promise<T> {
|
||||||
|
return this.http.request<T>(method, endpoint, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get account info
|
||||||
|
*/
|
||||||
|
public async getAccountInfo(): Promise<unknown> {
|
||||||
|
return this.request('GET', '/ea/account');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all sites (convenience method)
|
||||||
|
*/
|
||||||
|
public async getSites() {
|
||||||
|
return this.siteManager.listSites();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all hosts (convenience method)
|
||||||
|
*/
|
||||||
|
public async getHosts() {
|
||||||
|
return this.hostManager.listHosts();
|
||||||
|
}
|
||||||
|
}
|
||||||
377
ts/classes.unifi-controller.ts
Normal file
377
ts/classes.unifi-controller.ts
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import { logger } from './unifi.logger.js';
|
||||||
|
import { UnifiHttp } from './classes.unifihttp.js';
|
||||||
|
import { DeviceManager } from './classes.devicemanager.js';
|
||||||
|
import { ClientManager } from './classes.clientmanager.js';
|
||||||
|
import type {
|
||||||
|
IUnifiControllerOptions,
|
||||||
|
INetworkAuthResponse,
|
||||||
|
INetworkSite,
|
||||||
|
IUnifiApiResponse,
|
||||||
|
THttpMethod,
|
||||||
|
} from './interfaces/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UniFi Controller - Entry point for Network Controller API
|
||||||
|
*
|
||||||
|
* This class provides access to the UniFi Network Controller API.
|
||||||
|
* Supports two authentication methods:
|
||||||
|
* 1. API Key (preferred) - Set X-API-Key header, no login required
|
||||||
|
* 2. Session auth - Username/password login with session cookies
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // Using API key (no login required)
|
||||||
|
* const controller = new UnifiController({
|
||||||
|
* host: '192.168.1.1',
|
||||||
|
* apiKey: 'your-api-key',
|
||||||
|
* });
|
||||||
|
* const devices = await controller.deviceManager.listDevices();
|
||||||
|
*
|
||||||
|
* // Using session auth (requires login)
|
||||||
|
* const controller = new UnifiController({
|
||||||
|
* host: '192.168.1.1',
|
||||||
|
* username: 'admin',
|
||||||
|
* password: 'password',
|
||||||
|
* });
|
||||||
|
* await controller.login();
|
||||||
|
* const devices = await controller.deviceManager.listDevices();
|
||||||
|
* await controller.logout();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class UnifiController {
|
||||||
|
/** Controller host */
|
||||||
|
private host: string;
|
||||||
|
|
||||||
|
/** API key for authentication */
|
||||||
|
private apiKey?: string;
|
||||||
|
|
||||||
|
/** Username for session authentication */
|
||||||
|
private username?: string;
|
||||||
|
|
||||||
|
/** Password for session authentication */
|
||||||
|
private password?: string;
|
||||||
|
|
||||||
|
/** Controller type determines API paths */
|
||||||
|
private controllerType: 'unifi-os' | 'udm-pro' | 'standalone';
|
||||||
|
|
||||||
|
/** Whether to verify SSL certificates */
|
||||||
|
private verifySsl: boolean;
|
||||||
|
|
||||||
|
/** HTTP client */
|
||||||
|
private http: UnifiHttp;
|
||||||
|
|
||||||
|
/** Whether currently authenticated */
|
||||||
|
private authenticated: boolean = false;
|
||||||
|
|
||||||
|
/** CSRF token (for UniFi OS session auth) */
|
||||||
|
private csrfToken?: string;
|
||||||
|
|
||||||
|
/** Device manager instance */
|
||||||
|
public deviceManager: DeviceManager;
|
||||||
|
|
||||||
|
/** Client manager instance */
|
||||||
|
public clientManager: ClientManager;
|
||||||
|
|
||||||
|
constructor(options: IUnifiControllerOptions) {
|
||||||
|
this.host = options.host.replace(/\/$/, '');
|
||||||
|
this.apiKey = options.apiKey;
|
||||||
|
this.username = options.username;
|
||||||
|
this.password = options.password;
|
||||||
|
this.controllerType = options.controllerType || 'unifi-os';
|
||||||
|
this.verifySsl = options.verifySsl ?? false;
|
||||||
|
|
||||||
|
// Build base URL based on controller type
|
||||||
|
const baseUrl = this.getBaseUrl();
|
||||||
|
this.http = new UnifiHttp(baseUrl, this.verifySsl);
|
||||||
|
|
||||||
|
// If API key provided, set it and mark as authenticated
|
||||||
|
if (this.apiKey) {
|
||||||
|
this.http.setHeader('X-API-Key', this.apiKey);
|
||||||
|
this.authenticated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize managers
|
||||||
|
this.deviceManager = new DeviceManager(this);
|
||||||
|
this.clientManager = new ClientManager(this);
|
||||||
|
|
||||||
|
logger.log('info', `UnifiController initialized for ${this.host} (${this.controllerType})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the base URL for API requests based on controller type
|
||||||
|
*/
|
||||||
|
private getBaseUrl(): string {
|
||||||
|
// Add https:// if not present
|
||||||
|
const host = this.host.startsWith('http') ? this.host : `https://${this.host}`;
|
||||||
|
|
||||||
|
switch (this.controllerType) {
|
||||||
|
case 'unifi-os':
|
||||||
|
case 'udm-pro':
|
||||||
|
// UniFi OS consoles (UDM, UDM Pro, Cloud Key Gen2+) use /proxy/network prefix
|
||||||
|
return `${host}/proxy/network`;
|
||||||
|
case 'standalone':
|
||||||
|
// Standalone controllers (software controller)
|
||||||
|
return host;
|
||||||
|
default:
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the login endpoint based on controller type
|
||||||
|
*/
|
||||||
|
private getLoginEndpoint(): string {
|
||||||
|
switch (this.controllerType) {
|
||||||
|
case 'unifi-os':
|
||||||
|
case 'udm-pro':
|
||||||
|
// UniFi OS uses the root /api/auth/login
|
||||||
|
return '/api/auth/login';
|
||||||
|
case 'standalone':
|
||||||
|
return '/api/login';
|
||||||
|
default:
|
||||||
|
return '/api/login';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the logout endpoint based on controller type
|
||||||
|
*/
|
||||||
|
private getLogoutEndpoint(): string {
|
||||||
|
switch (this.controllerType) {
|
||||||
|
case 'unifi-os':
|
||||||
|
case 'udm-pro':
|
||||||
|
return '/api/auth/logout';
|
||||||
|
case 'standalone':
|
||||||
|
return '/api/logout';
|
||||||
|
default:
|
||||||
|
return '/api/logout';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login to the controller (only needed for session auth, not API key)
|
||||||
|
*/
|
||||||
|
public async login(): Promise<void> {
|
||||||
|
// If using API key, already authenticated
|
||||||
|
if (this.apiKey) {
|
||||||
|
logger.log('info', 'Using API key authentication, no login required');
|
||||||
|
this.authenticated = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.username || !this.password) {
|
||||||
|
throw new Error('Username and password required for session authentication');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', `Logging in to UniFi Controller at ${this.host}`);
|
||||||
|
|
||||||
|
// For UniFi OS, login happens at the console level (not through /proxy/network)
|
||||||
|
let loginUrl: string;
|
||||||
|
if (this.controllerType === 'unifi-os' || this.controllerType === 'udm-pro') {
|
||||||
|
const host = this.host.startsWith('http') ? this.host : `https://${this.host}`;
|
||||||
|
loginUrl = `${host}${this.getLoginEndpoint()}`;
|
||||||
|
} else {
|
||||||
|
loginUrl = `${this.getBaseUrl()}${this.getLoginEndpoint()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a separate HTTP client for login (different base URL for UniFi OS)
|
||||||
|
const loginHttp = new UnifiHttp(
|
||||||
|
loginUrl.replace(this.getLoginEndpoint(), ''),
|
||||||
|
this.verifySsl
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await loginHttp.rawRequest('POST', this.getLoginEndpoint(), {
|
||||||
|
username: this.username,
|
||||||
|
password: this.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text().catch(() => 'Unknown error');
|
||||||
|
throw new Error(`Login failed: HTTP ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store cookies from response
|
||||||
|
const setCookieHeaders = response.headers?.['set-cookie'];
|
||||||
|
if (setCookieHeaders && Array.isArray(setCookieHeaders)) {
|
||||||
|
this.http.setCookies(setCookieHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract CSRF token from response (UniFi OS)
|
||||||
|
const csrfHeader = response.headers?.['x-csrf-token'];
|
||||||
|
if (csrfHeader) {
|
||||||
|
this.csrfToken = Array.isArray(csrfHeader) ? csrfHeader[0] : csrfHeader;
|
||||||
|
this.http.setHeader('X-CSRF-Token', this.csrfToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.authenticated = true;
|
||||||
|
logger.log('info', 'Successfully logged in to UniFi Controller');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout from the controller (only for session auth)
|
||||||
|
*/
|
||||||
|
public async logout(): Promise<void> {
|
||||||
|
// If using API key, nothing to logout
|
||||||
|
if (this.apiKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.authenticated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', 'Logging out from UniFi Controller');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For UniFi OS, logout happens at the console level
|
||||||
|
if (this.controllerType === 'unifi-os' || this.controllerType === 'udm-pro') {
|
||||||
|
const host = this.host.startsWith('http') ? this.host : `https://${this.host}`;
|
||||||
|
const logoutHttp = new UnifiHttp(host, this.verifySsl);
|
||||||
|
logoutHttp.setCookies([this.http.getCookieHeader()]);
|
||||||
|
if (this.csrfToken) {
|
||||||
|
logoutHttp.setHeader('X-CSRF-Token', this.csrfToken);
|
||||||
|
}
|
||||||
|
await logoutHttp.rawRequest('POST', this.getLogoutEndpoint());
|
||||||
|
} else {
|
||||||
|
await this.http.rawRequest('POST', this.getLogoutEndpoint());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log('warn', `Logout error (may be expected): ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.authenticated = false;
|
||||||
|
this.csrfToken = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if logged in
|
||||||
|
*/
|
||||||
|
public isAuthenticated(): boolean {
|
||||||
|
return this.authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a request to the controller API
|
||||||
|
*/
|
||||||
|
public async request<T>(
|
||||||
|
method: THttpMethod,
|
||||||
|
endpoint: string,
|
||||||
|
data?: unknown
|
||||||
|
): Promise<T> {
|
||||||
|
if (!this.authenticated) {
|
||||||
|
throw new Error('Not authenticated. Call login() first or provide API key.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.request<T>(method, endpoint, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all sites on this controller
|
||||||
|
*/
|
||||||
|
public async listSites(): Promise<INetworkSite[]> {
|
||||||
|
logger.log('debug', 'Fetching sites');
|
||||||
|
|
||||||
|
const response = await this.request<IUnifiApiResponse<INetworkSite>>(
|
||||||
|
'GET',
|
||||||
|
'/api/self/sites'
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get system info
|
||||||
|
*/
|
||||||
|
public async getSystemInfo(): Promise<unknown> {
|
||||||
|
return this.request('GET', '/api/s/default/stat/sysinfo');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get controller health
|
||||||
|
*/
|
||||||
|
public async getHealth(siteId: string = 'default'): Promise<unknown> {
|
||||||
|
return this.request('GET', `/api/s/${siteId}/stat/health`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active alerts
|
||||||
|
*/
|
||||||
|
public async getAlerts(siteId: string = 'default'): Promise<unknown> {
|
||||||
|
return this.request('GET', `/api/s/${siteId}/stat/alarm`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get events
|
||||||
|
*/
|
||||||
|
public async getEvents(
|
||||||
|
siteId: string = 'default',
|
||||||
|
options: { start?: number; end?: number; limit?: number } = {}
|
||||||
|
): Promise<unknown> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (options.start) params.append('start', options.start.toString());
|
||||||
|
if (options.end) params.append('end', options.end.toString());
|
||||||
|
if (options.limit) params.append('_limit', options.limit.toString());
|
||||||
|
|
||||||
|
const queryString = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
return this.request('GET', `/api/s/${siteId}/stat/event${queryString}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get WLAN configurations
|
||||||
|
*/
|
||||||
|
public async getWlans(siteId: string = 'default'): Promise<unknown> {
|
||||||
|
return this.request('GET', `/api/s/${siteId}/rest/wlanconf`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get network configurations
|
||||||
|
*/
|
||||||
|
public async getNetworks(siteId: string = 'default'): Promise<unknown> {
|
||||||
|
return this.request('GET', `/api/s/${siteId}/rest/networkconf`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get port forward rules
|
||||||
|
*/
|
||||||
|
public async getPortForwards(siteId: string = 'default'): Promise<unknown> {
|
||||||
|
return this.request('GET', `/api/s/${siteId}/rest/portforward`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get firewall rules
|
||||||
|
*/
|
||||||
|
public async getFirewallRules(siteId: string = 'default'): Promise<unknown> {
|
||||||
|
return this.request('GET', `/api/s/${siteId}/rest/firewallrule`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get DPI stats
|
||||||
|
*/
|
||||||
|
public async getDpiStats(siteId: string = 'default'): Promise<unknown> {
|
||||||
|
return this.request('GET', `/api/s/${siteId}/stat/dpi`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup the controller configuration
|
||||||
|
*/
|
||||||
|
public async createBackup(siteId: string = 'default'): Promise<unknown> {
|
||||||
|
return this.request('POST', `/api/s/${siteId}/cmd/backup`, {
|
||||||
|
cmd: 'backup',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get devices (convenience method)
|
||||||
|
*/
|
||||||
|
public async getDevices(siteId: string = 'default') {
|
||||||
|
return this.deviceManager.listDevices(siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active clients (convenience method)
|
||||||
|
*/
|
||||||
|
public async getClients(siteId: string = 'default') {
|
||||||
|
return this.clientManager.listActiveClients(siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
343
ts/classes.unifi-protect.ts
Normal file
343
ts/classes.unifi-protect.ts
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import { logger } from './unifi.logger.js';
|
||||||
|
import { UnifiHttp } from './classes.unifihttp.js';
|
||||||
|
import { CameraManager } from './classes.cameramanager.js';
|
||||||
|
import type {
|
||||||
|
IUnifiProtectOptions,
|
||||||
|
IProtectBootstrap,
|
||||||
|
IProtectCamera,
|
||||||
|
IProtectNvr,
|
||||||
|
THttpMethod,
|
||||||
|
} from './interfaces/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UniFi Protect - Entry point for Protect NVR API
|
||||||
|
*
|
||||||
|
* This class provides access to the UniFi Protect API for managing cameras,
|
||||||
|
* recordings, and motion events. Supports two authentication methods:
|
||||||
|
* 1. API Key (preferred) - Set X-API-Key header, no login required
|
||||||
|
* 2. Session auth - Username/password login with session cookies + CSRF
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // Using API key (no login required)
|
||||||
|
* const protect = new UnifiProtect({
|
||||||
|
* host: '192.168.1.1',
|
||||||
|
* apiKey: 'your-api-key',
|
||||||
|
* });
|
||||||
|
* await protect.refreshBootstrap(); // Load camera data
|
||||||
|
* const cameras = await protect.cameraManager.listCameras();
|
||||||
|
*
|
||||||
|
* // Using session auth
|
||||||
|
* const protect = new UnifiProtect({
|
||||||
|
* host: '192.168.1.1',
|
||||||
|
* username: 'admin',
|
||||||
|
* password: 'password',
|
||||||
|
* });
|
||||||
|
* await protect.login();
|
||||||
|
* const cameras = await protect.cameraManager.listCameras();
|
||||||
|
* await protect.logout();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class UnifiProtect {
|
||||||
|
/** Protect host */
|
||||||
|
private host: string;
|
||||||
|
|
||||||
|
/** API key for authentication */
|
||||||
|
private apiKey?: string;
|
||||||
|
|
||||||
|
/** Username for session authentication */
|
||||||
|
private username?: string;
|
||||||
|
|
||||||
|
/** Password for session authentication */
|
||||||
|
private password?: string;
|
||||||
|
|
||||||
|
/** Whether to verify SSL certificates */
|
||||||
|
private verifySsl: boolean;
|
||||||
|
|
||||||
|
/** HTTP client for Protect API */
|
||||||
|
private http: UnifiHttp;
|
||||||
|
|
||||||
|
/** HTTP client for auth (console level) */
|
||||||
|
private authHttp: UnifiHttp;
|
||||||
|
|
||||||
|
/** Whether currently authenticated */
|
||||||
|
private authenticated: boolean = false;
|
||||||
|
|
||||||
|
/** CSRF token */
|
||||||
|
private csrfToken?: string;
|
||||||
|
|
||||||
|
/** Bootstrap data (contains all cameras, NVR info, etc.) */
|
||||||
|
private bootstrap?: IProtectBootstrap;
|
||||||
|
|
||||||
|
/** Camera manager instance */
|
||||||
|
public cameraManager: CameraManager;
|
||||||
|
|
||||||
|
constructor(options: IUnifiProtectOptions) {
|
||||||
|
this.host = options.host.replace(/\/$/, '');
|
||||||
|
this.apiKey = options.apiKey;
|
||||||
|
this.username = options.username;
|
||||||
|
this.password = options.password;
|
||||||
|
this.verifySsl = options.verifySsl ?? false;
|
||||||
|
|
||||||
|
// Build base URLs
|
||||||
|
const baseHost = this.host.startsWith('http') ? this.host : `https://${this.host}`;
|
||||||
|
|
||||||
|
// Auth happens at console level
|
||||||
|
this.authHttp = new UnifiHttp(baseHost, this.verifySsl);
|
||||||
|
|
||||||
|
// Protect API is behind /proxy/protect/api
|
||||||
|
this.http = new UnifiHttp(`${baseHost}/proxy/protect/api`, this.verifySsl);
|
||||||
|
|
||||||
|
// If API key provided, set it and mark as authenticated
|
||||||
|
if (this.apiKey) {
|
||||||
|
this.http.setHeader('X-API-Key', this.apiKey);
|
||||||
|
this.authenticated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize managers
|
||||||
|
this.cameraManager = new CameraManager(this);
|
||||||
|
|
||||||
|
logger.log('info', `UnifiProtect initialized for ${this.host}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login to Protect (only needed for session auth, not API key)
|
||||||
|
*/
|
||||||
|
public async login(): Promise<void> {
|
||||||
|
// If using API key, already authenticated
|
||||||
|
if (this.apiKey) {
|
||||||
|
logger.log('info', 'Using API key authentication, no login required');
|
||||||
|
this.authenticated = true;
|
||||||
|
await this.fetchBootstrap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.username || !this.password) {
|
||||||
|
throw new Error('Username and password required for session authentication');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', `Logging in to UniFi Protect at ${this.host}`);
|
||||||
|
|
||||||
|
// Login at console level
|
||||||
|
const response = await this.authHttp.rawRequest('POST', '/api/auth/login', {
|
||||||
|
username: this.username,
|
||||||
|
password: this.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text().catch(() => 'Unknown error');
|
||||||
|
throw new Error(`Protect login failed: HTTP ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store cookies
|
||||||
|
const setCookieHeaders = response.headers?.['set-cookie'];
|
||||||
|
if (setCookieHeaders && Array.isArray(setCookieHeaders)) {
|
||||||
|
this.authHttp.setCookies(setCookieHeaders);
|
||||||
|
this.http.setCookies(setCookieHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract CSRF token
|
||||||
|
const csrfHeader = response.headers?.['x-csrf-token'];
|
||||||
|
if (csrfHeader) {
|
||||||
|
this.csrfToken = Array.isArray(csrfHeader) ? csrfHeader[0] : csrfHeader;
|
||||||
|
this.http.setHeader('X-CSRF-Token', this.csrfToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.authenticated = true;
|
||||||
|
|
||||||
|
// Fetch bootstrap to get cameras and NVR info
|
||||||
|
await this.fetchBootstrap();
|
||||||
|
|
||||||
|
logger.log('info', 'Successfully logged in to UniFi Protect');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout from Protect (only for session auth)
|
||||||
|
*/
|
||||||
|
public async logout(): Promise<void> {
|
||||||
|
// If using API key, nothing to logout
|
||||||
|
if (this.apiKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.authenticated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', 'Logging out from UniFi Protect');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.authHttp.rawRequest('POST', '/api/auth/logout');
|
||||||
|
} catch (error) {
|
||||||
|
logger.log('warn', `Protect logout error (may be expected): ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.authenticated = false;
|
||||||
|
this.csrfToken = undefined;
|
||||||
|
this.bootstrap = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if logged in
|
||||||
|
*/
|
||||||
|
public isAuthenticated(): boolean {
|
||||||
|
return this.authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch bootstrap data (cameras, NVR info, etc.)
|
||||||
|
*/
|
||||||
|
private async fetchBootstrap(): Promise<void> {
|
||||||
|
logger.log('debug', 'Fetching Protect bootstrap');
|
||||||
|
|
||||||
|
this.bootstrap = await this.http.request<IProtectBootstrap>('GET', '/bootstrap');
|
||||||
|
|
||||||
|
logger.log('info', `Bootstrap loaded: ${this.bootstrap.cameras?.length || 0} cameras`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh bootstrap data
|
||||||
|
*/
|
||||||
|
public async refreshBootstrap(): Promise<void> {
|
||||||
|
await this.fetchBootstrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get NVR info from bootstrap
|
||||||
|
*/
|
||||||
|
public getNvrInfo(): IProtectNvr | undefined {
|
||||||
|
return this.bootstrap?.nvr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cameras from bootstrap (cached)
|
||||||
|
*/
|
||||||
|
public getCamerasFromBootstrap(): IProtectCamera[] {
|
||||||
|
return this.bootstrap?.cameras || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a request to the Protect API
|
||||||
|
*/
|
||||||
|
public async request<T>(
|
||||||
|
method: THttpMethod,
|
||||||
|
endpoint: string,
|
||||||
|
data?: unknown
|
||||||
|
): Promise<T> {
|
||||||
|
if (!this.authenticated) {
|
||||||
|
throw new Error('Not authenticated. Call login() first or provide API key.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.request<T>(method, endpoint, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all cameras (convenience method)
|
||||||
|
*/
|
||||||
|
public async getCameras() {
|
||||||
|
return this.cameraManager.listCameras();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get storage info from NVR
|
||||||
|
*/
|
||||||
|
public getStorageInfo(): { totalSize?: number; usedSpace?: number; freeSpace?: number } | undefined {
|
||||||
|
const nvr = this.getNvrInfo();
|
||||||
|
if (!nvr?.storageInfo) return undefined;
|
||||||
|
|
||||||
|
const totalSize = nvr.storageInfo.totalSize;
|
||||||
|
const usedSpace = nvr.storageInfo.totalSpaceUsed;
|
||||||
|
const freeSpace = totalSize && usedSpace ? totalSize - usedSpace : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalSize,
|
||||||
|
usedSpace,
|
||||||
|
freeSpace,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get NVR uptime
|
||||||
|
*/
|
||||||
|
public getNvrUptime(): number | undefined {
|
||||||
|
return this.getNvrInfo()?.uptime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if NVR is connected to cloud
|
||||||
|
*/
|
||||||
|
public isCloudConnected(): boolean {
|
||||||
|
return this.getNvrInfo()?.isConnectedToCloud === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get system events
|
||||||
|
*/
|
||||||
|
public async getSystemEvents(limit: number = 100): Promise<unknown> {
|
||||||
|
return this.request('GET', `/events?limit=${limit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get liveviews
|
||||||
|
*/
|
||||||
|
public async getLiveviews(): Promise<unknown> {
|
||||||
|
return this.request('GET', '/liveviews');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get users
|
||||||
|
*/
|
||||||
|
public async getUsers(): Promise<unknown> {
|
||||||
|
return this.request('GET', '/users');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get groups
|
||||||
|
*/
|
||||||
|
public async getGroups(): Promise<unknown> {
|
||||||
|
return this.request('GET', '/groups');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get lights (if any)
|
||||||
|
*/
|
||||||
|
public getLights() {
|
||||||
|
return this.bootstrap?.lights || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sensors (if any)
|
||||||
|
*/
|
||||||
|
public getSensors() {
|
||||||
|
return this.bootstrap?.sensors || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get doorbells
|
||||||
|
*/
|
||||||
|
public getDoorbells() {
|
||||||
|
return this.bootstrap?.doorbells || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get chimes
|
||||||
|
*/
|
||||||
|
public getChimes() {
|
||||||
|
return this.bootstrap?.chimes || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bridges
|
||||||
|
*/
|
||||||
|
public getBridges() {
|
||||||
|
return this.bootstrap?.bridges || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get viewers
|
||||||
|
*/
|
||||||
|
public getViewers() {
|
||||||
|
return this.bootstrap?.viewers || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
265
ts/classes.unifihttp.ts
Normal file
265
ts/classes.unifihttp.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import { logger } from './unifi.logger.js';
|
||||||
|
import type { THttpMethod } from './interfaces/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended response type
|
||||||
|
*/
|
||||||
|
export interface IUnifiHttpResponse<T = any> {
|
||||||
|
ok: boolean;
|
||||||
|
status: number;
|
||||||
|
statusText?: string;
|
||||||
|
headers?: Record<string, string | string[]>;
|
||||||
|
body: T;
|
||||||
|
text: () => Promise<string>;
|
||||||
|
json: () => Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base HTTP client for UniFi APIs with SSL handling and authentication support
|
||||||
|
*/
|
||||||
|
export class UnifiHttp {
|
||||||
|
private baseUrl: string;
|
||||||
|
private headers: Record<string, string> = {};
|
||||||
|
private verifySsl: boolean;
|
||||||
|
private cookies: string[] = [];
|
||||||
|
|
||||||
|
constructor(baseUrl: string, verifySsl: boolean = false) {
|
||||||
|
this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
||||||
|
this.verifySsl = verifySsl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a header value
|
||||||
|
*/
|
||||||
|
public setHeader(name: string, value: string): void {
|
||||||
|
this.headers[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a header
|
||||||
|
*/
|
||||||
|
public removeHeader(name: string): void {
|
||||||
|
delete this.headers[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set cookies from Set-Cookie headers
|
||||||
|
*/
|
||||||
|
public setCookies(cookies: string[]): void {
|
||||||
|
this.cookies = cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cookie header string
|
||||||
|
*/
|
||||||
|
public getCookieHeader(): string {
|
||||||
|
return this.cookies
|
||||||
|
.map((cookie) => cookie.split(';')[0])
|
||||||
|
.join('; ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build request with common options
|
||||||
|
*/
|
||||||
|
private buildRequest(url: string, data?: unknown): plugins.smartrequest.SmartRequestClient {
|
||||||
|
let requestBuilder = plugins.smartrequest.SmartRequestClient.create()
|
||||||
|
.url(url)
|
||||||
|
.header('Content-Type', 'application/json');
|
||||||
|
|
||||||
|
// Add stored headers
|
||||||
|
for (const [name, value] of Object.entries(this.headers)) {
|
||||||
|
requestBuilder = requestBuilder.header(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add cookies
|
||||||
|
const cookieHeader = this.getCookieHeader();
|
||||||
|
if (cookieHeader) {
|
||||||
|
requestBuilder = requestBuilder.header('Cookie', cookieHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add JSON data
|
||||||
|
if (data) {
|
||||||
|
requestBuilder = requestBuilder.json(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an HTTP request
|
||||||
|
*/
|
||||||
|
public async request<T>(
|
||||||
|
method: THttpMethod,
|
||||||
|
endpoint: string,
|
||||||
|
data?: unknown
|
||||||
|
): Promise<T> {
|
||||||
|
const url = `${this.baseUrl}${endpoint}`;
|
||||||
|
|
||||||
|
logger.log('debug', `UniFi HTTP ${method} ${url}`);
|
||||||
|
|
||||||
|
const requestBuilder = this.buildRequest(url, data);
|
||||||
|
|
||||||
|
let response: plugins.smartrequest.IExtendedIncomingMessage;
|
||||||
|
|
||||||
|
// Note: smartrequest v2 doesn't have built-in SSL bypass, but Node's default is fine for most cases
|
||||||
|
// For self-signed certs, we need to handle at the environment level or use NODE_TLS_REJECT_UNAUTHORIZED
|
||||||
|
const originalRejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
||||||
|
if (!this.verifySsl) {
|
||||||
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (method) {
|
||||||
|
case 'GET':
|
||||||
|
response = await requestBuilder.get();
|
||||||
|
break;
|
||||||
|
case 'POST':
|
||||||
|
response = await requestBuilder.post();
|
||||||
|
break;
|
||||||
|
case 'PUT':
|
||||||
|
response = await requestBuilder.put();
|
||||||
|
break;
|
||||||
|
case 'DELETE':
|
||||||
|
response = await requestBuilder.delete();
|
||||||
|
break;
|
||||||
|
case 'PATCH':
|
||||||
|
response = await requestBuilder.patch();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported HTTP method: ${method}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!this.verifySsl) {
|
||||||
|
if (originalRejectUnauthorized !== undefined) {
|
||||||
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalRejectUnauthorized;
|
||||||
|
} else {
|
||||||
|
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store cookies from response
|
||||||
|
const setCookieHeaders = response.headers?.['set-cookie'];
|
||||||
|
if (setCookieHeaders && Array.isArray(setCookieHeaders)) {
|
||||||
|
this.setCookies(setCookieHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for HTTP errors
|
||||||
|
const statusCode = response.statusCode || 0;
|
||||||
|
if (statusCode >= 400) {
|
||||||
|
const errorBody = typeof response.body === 'string' ? response.body : JSON.stringify(response.body);
|
||||||
|
logger.log('error', `UniFi HTTP error: ${statusCode} - ${errorBody}`);
|
||||||
|
throw new Error(`HTTP ${statusCode}: ${response.statusMessage || 'Unknown'} - ${errorBody}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return body directly
|
||||||
|
return response.body as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a raw request returning the full response (for handling cookies/headers)
|
||||||
|
*/
|
||||||
|
public async rawRequest(
|
||||||
|
method: THttpMethod,
|
||||||
|
endpoint: string,
|
||||||
|
data?: unknown
|
||||||
|
): Promise<IUnifiHttpResponse> {
|
||||||
|
const url = `${this.baseUrl}${endpoint}`;
|
||||||
|
|
||||||
|
logger.log('debug', `UniFi HTTP raw ${method} ${url}`);
|
||||||
|
|
||||||
|
const requestBuilder = this.buildRequest(url, data);
|
||||||
|
|
||||||
|
let response: plugins.smartrequest.IExtendedIncomingMessage;
|
||||||
|
|
||||||
|
const originalRejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
||||||
|
if (!this.verifySsl) {
|
||||||
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (method) {
|
||||||
|
case 'GET':
|
||||||
|
response = await requestBuilder.get();
|
||||||
|
break;
|
||||||
|
case 'POST':
|
||||||
|
response = await requestBuilder.post();
|
||||||
|
break;
|
||||||
|
case 'PUT':
|
||||||
|
response = await requestBuilder.put();
|
||||||
|
break;
|
||||||
|
case 'DELETE':
|
||||||
|
response = await requestBuilder.delete();
|
||||||
|
break;
|
||||||
|
case 'PATCH':
|
||||||
|
response = await requestBuilder.patch();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported HTTP method: ${method}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!this.verifySsl) {
|
||||||
|
if (originalRejectUnauthorized !== undefined) {
|
||||||
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalRejectUnauthorized;
|
||||||
|
} else {
|
||||||
|
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store cookies from response
|
||||||
|
const setCookieHeaders = response.headers?.['set-cookie'];
|
||||||
|
if (setCookieHeaders && Array.isArray(setCookieHeaders)) {
|
||||||
|
this.setCookies(setCookieHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusCode = response.statusCode || 0;
|
||||||
|
const body = response.body;
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: statusCode >= 200 && statusCode < 300,
|
||||||
|
status: statusCode,
|
||||||
|
statusText: response.statusMessage,
|
||||||
|
headers: response.headers as Record<string, string | string[]>,
|
||||||
|
body,
|
||||||
|
text: async () => typeof body === 'string' ? body : JSON.stringify(body),
|
||||||
|
json: async () => typeof body === 'object' ? body : JSON.parse(body as string),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for GET requests
|
||||||
|
*/
|
||||||
|
public async get<T>(endpoint: string): Promise<T> {
|
||||||
|
return this.request<T>('GET', endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for POST requests
|
||||||
|
*/
|
||||||
|
public async post<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||||
|
return this.request<T>('POST', endpoint, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for PUT requests
|
||||||
|
*/
|
||||||
|
public async put<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||||
|
return this.request<T>('PUT', endpoint, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for DELETE requests
|
||||||
|
*/
|
||||||
|
public async delete<T>(endpoint: string): Promise<T> {
|
||||||
|
return this.request<T>('DELETE', endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for PATCH requests
|
||||||
|
*/
|
||||||
|
public async patch<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||||
|
return this.request<T>('PATCH', endpoint, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
ts/index.ts
38
ts/index.ts
@@ -1,3 +1,37 @@
|
|||||||
import * as plugins from './plugins.js';
|
/**
|
||||||
|
* @apiclient.xyz/unifi
|
||||||
|
*
|
||||||
|
* A comprehensive UniFi API client supporting multiple UniFi applications:
|
||||||
|
* - Site Manager API (cloud) - API key authentication
|
||||||
|
* - Network Controller API (local) - Session cookie authentication
|
||||||
|
* - Protect API (local) - Session + CSRF token authentication
|
||||||
|
* - Access API (local) - Bearer token authentication
|
||||||
|
*/
|
||||||
|
|
||||||
export let demoExport = 'Hi there! :) This is an exported string';
|
// Re-export all interfaces
|
||||||
|
export * from './interfaces/index.js';
|
||||||
|
|
||||||
|
// Export entry point classes
|
||||||
|
export { UnifiAccount } from './classes.unifi-account.js';
|
||||||
|
export { UnifiController } from './classes.unifi-controller.js';
|
||||||
|
export { UnifiProtect } from './classes.unifi-protect.js';
|
||||||
|
export { UnifiAccess } from './classes.unifi-access.js';
|
||||||
|
|
||||||
|
// Export manager classes
|
||||||
|
export { SiteManager } from './classes.sitemanager.js';
|
||||||
|
export { HostManager } from './classes.hostmanager.js';
|
||||||
|
export { DeviceManager } from './classes.devicemanager.js';
|
||||||
|
export { ClientManager } from './classes.clientmanager.js';
|
||||||
|
export { CameraManager } from './classes.cameramanager.js';
|
||||||
|
export { DoorManager } from './classes.doormanager.js';
|
||||||
|
|
||||||
|
// Export resource classes
|
||||||
|
export { UnifiSite } from './classes.site.js';
|
||||||
|
export { UnifiHost } from './classes.host.js';
|
||||||
|
export { UnifiDevice } from './classes.device.js';
|
||||||
|
export { UnifiClient } from './classes.client.js';
|
||||||
|
export { UnifiCamera } from './classes.camera.js';
|
||||||
|
export { UnifiDoor } from './classes.door.js';
|
||||||
|
|
||||||
|
// Export HTTP client for advanced usage
|
||||||
|
export { UnifiHttp } from './classes.unifihttp.js';
|
||||||
|
|||||||
284
ts/interfaces/access.ts
Normal file
284
ts/interfaces/access.ts
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
/**
|
||||||
|
* Access API interfaces
|
||||||
|
* Base URL: https://{host}:12445/api/v1/developer
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access device (door controller, hub, reader)
|
||||||
|
*/
|
||||||
|
export interface IAccessDevice {
|
||||||
|
/** Device unique ID */
|
||||||
|
unique_id: string;
|
||||||
|
/** Device name */
|
||||||
|
name: string;
|
||||||
|
/** Device alias */
|
||||||
|
alias?: string;
|
||||||
|
/** Device model */
|
||||||
|
device_type: string;
|
||||||
|
/** Hardware revision */
|
||||||
|
revision?: number;
|
||||||
|
/** Firmware version */
|
||||||
|
version?: string;
|
||||||
|
/** Firmware version */
|
||||||
|
version_update_to?: string;
|
||||||
|
/** Adopted */
|
||||||
|
adopted?: boolean;
|
||||||
|
/** Connected */
|
||||||
|
connected?: boolean;
|
||||||
|
/** IP address */
|
||||||
|
ip?: string;
|
||||||
|
/** MAC address */
|
||||||
|
mac?: string;
|
||||||
|
/** Start timestamp */
|
||||||
|
start_time?: number;
|
||||||
|
/** Security level */
|
||||||
|
security_check?: boolean;
|
||||||
|
/** Location */
|
||||||
|
location?: IAccessLocation;
|
||||||
|
/** Capabilities */
|
||||||
|
capabilities?: string[];
|
||||||
|
/** Device configuration */
|
||||||
|
configs?: IAccessDeviceConfig[];
|
||||||
|
/** Connected devices (for hub) */
|
||||||
|
connected_devices?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access door
|
||||||
|
*/
|
||||||
|
export interface IAccessDoor {
|
||||||
|
/** Door unique ID */
|
||||||
|
unique_id: string;
|
||||||
|
/** Door name */
|
||||||
|
name: string;
|
||||||
|
/** Door alias */
|
||||||
|
alias?: string;
|
||||||
|
/** Door type */
|
||||||
|
door_type?: string;
|
||||||
|
/** Whether door is locked */
|
||||||
|
door_lock_relay_status?: 'lock' | 'unlock';
|
||||||
|
/** Whether door is open (contact sensor) */
|
||||||
|
door_position_status?: 'open' | 'close';
|
||||||
|
/** Door controller device ID */
|
||||||
|
device_id?: string;
|
||||||
|
/** Associated camera ID */
|
||||||
|
camera_resource_id?: string;
|
||||||
|
/** Location */
|
||||||
|
location_id?: string;
|
||||||
|
/** Full */
|
||||||
|
full_name?: string;
|
||||||
|
/** Extra type */
|
||||||
|
extra_type?: string;
|
||||||
|
/** Door guard */
|
||||||
|
door_guard?: boolean;
|
||||||
|
/** Rules */
|
||||||
|
rules?: IAccessDoorRule[];
|
||||||
|
/** Level ID */
|
||||||
|
level_id?: string;
|
||||||
|
/** Floor info */
|
||||||
|
floor?: IAccessFloor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Door rule
|
||||||
|
*/
|
||||||
|
export interface IAccessDoorRule {
|
||||||
|
/** Rule unique ID */
|
||||||
|
unique_id: string;
|
||||||
|
/** Rule name */
|
||||||
|
name?: string;
|
||||||
|
/** Rule type */
|
||||||
|
type?: string;
|
||||||
|
/** Enabled */
|
||||||
|
enabled?: boolean;
|
||||||
|
/** Interval start */
|
||||||
|
interval_start?: string;
|
||||||
|
/** Interval end */
|
||||||
|
interval_end?: string;
|
||||||
|
/** Days of week */
|
||||||
|
days?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access user/credential holder
|
||||||
|
*/
|
||||||
|
export interface IAccessUser {
|
||||||
|
/** User unique ID */
|
||||||
|
unique_id: string;
|
||||||
|
/** User ID */
|
||||||
|
id?: string;
|
||||||
|
/** First name */
|
||||||
|
first_name: string;
|
||||||
|
/** Last name */
|
||||||
|
last_name: string;
|
||||||
|
/** Full name */
|
||||||
|
full_name?: string;
|
||||||
|
/** Email */
|
||||||
|
email?: string;
|
||||||
|
/** Phone */
|
||||||
|
phone?: string;
|
||||||
|
/** Employee number */
|
||||||
|
employee_number?: string;
|
||||||
|
/** Status */
|
||||||
|
status?: 'active' | 'inactive' | 'pending';
|
||||||
|
/** Avatar */
|
||||||
|
avatar?: string;
|
||||||
|
/** PIN code (hashed) */
|
||||||
|
pin_code?: string;
|
||||||
|
/** NFC cards */
|
||||||
|
nfc_cards?: IAccessNfcCard[];
|
||||||
|
/** Access groups */
|
||||||
|
access_groups?: string[];
|
||||||
|
/** Notes */
|
||||||
|
notes?: string;
|
||||||
|
/** Start date */
|
||||||
|
start_date?: string;
|
||||||
|
/** End date */
|
||||||
|
end_date?: string;
|
||||||
|
/** Onboarding timestamp */
|
||||||
|
onboarding_timestamp?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NFC card
|
||||||
|
*/
|
||||||
|
export interface IAccessNfcCard {
|
||||||
|
/** Card unique ID */
|
||||||
|
unique_id?: string;
|
||||||
|
/** Token (card number) */
|
||||||
|
token: string;
|
||||||
|
/** Card type */
|
||||||
|
card_type?: string;
|
||||||
|
/** Card alias */
|
||||||
|
alias?: string;
|
||||||
|
/** Status */
|
||||||
|
status?: 'active' | 'inactive' | 'pending';
|
||||||
|
/** Is lost */
|
||||||
|
is_lost?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access policy/group
|
||||||
|
*/
|
||||||
|
export interface IAccessPolicy {
|
||||||
|
/** Policy unique ID */
|
||||||
|
unique_id: string;
|
||||||
|
/** Policy name */
|
||||||
|
name: string;
|
||||||
|
/** Description */
|
||||||
|
description?: string;
|
||||||
|
/** Resources (doors) */
|
||||||
|
resources?: IAccessPolicyResource[];
|
||||||
|
/** Schedules */
|
||||||
|
schedules?: IAccessSchedule[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Policy resource
|
||||||
|
*/
|
||||||
|
export interface IAccessPolicyResource {
|
||||||
|
/** Resource unique ID */
|
||||||
|
unique_id: string;
|
||||||
|
/** Resource type */
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule
|
||||||
|
*/
|
||||||
|
export interface IAccessSchedule {
|
||||||
|
/** Schedule unique ID */
|
||||||
|
unique_id: string;
|
||||||
|
/** Name */
|
||||||
|
name?: string;
|
||||||
|
/** Type */
|
||||||
|
type?: string;
|
||||||
|
/** Days */
|
||||||
|
days?: string[];
|
||||||
|
/** Start time */
|
||||||
|
start_time?: string;
|
||||||
|
/** End time */
|
||||||
|
end_time?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Location (building/area)
|
||||||
|
*/
|
||||||
|
export interface IAccessLocation {
|
||||||
|
/** Location unique ID */
|
||||||
|
unique_id: string;
|
||||||
|
/** Location name */
|
||||||
|
name: string;
|
||||||
|
/** Address */
|
||||||
|
address?: string;
|
||||||
|
/** Timezone */
|
||||||
|
timezone?: string;
|
||||||
|
/** Latitude */
|
||||||
|
latitude?: number;
|
||||||
|
/** Longitude */
|
||||||
|
longitude?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Floor
|
||||||
|
*/
|
||||||
|
export interface IAccessFloor {
|
||||||
|
/** Floor unique ID */
|
||||||
|
unique_id: string;
|
||||||
|
/** Floor name */
|
||||||
|
name: string;
|
||||||
|
/** Floor number */
|
||||||
|
number?: number;
|
||||||
|
/** Floor plan image */
|
||||||
|
floor_plan_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device configuration
|
||||||
|
*/
|
||||||
|
export interface IAccessDeviceConfig {
|
||||||
|
/** Config key */
|
||||||
|
key: string;
|
||||||
|
/** Config value */
|
||||||
|
value: string | number | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access event (entry/exit log)
|
||||||
|
*/
|
||||||
|
export interface IAccessEvent {
|
||||||
|
/** Event unique ID */
|
||||||
|
unique_id: string;
|
||||||
|
/** Event type */
|
||||||
|
type: 'access.door.unlock' | 'access.door.lock' | 'access.door.open' | 'access.door.close' | 'access.entry.granted' | 'access.entry.denied';
|
||||||
|
/** Timestamp */
|
||||||
|
timestamp: number;
|
||||||
|
/** Door ID */
|
||||||
|
door_id?: string;
|
||||||
|
/** User ID */
|
||||||
|
user_id?: string;
|
||||||
|
/** Device ID */
|
||||||
|
device_id?: string;
|
||||||
|
/** Reason */
|
||||||
|
reason?: string;
|
||||||
|
/** Extra data */
|
||||||
|
extra?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access API response wrapper
|
||||||
|
*/
|
||||||
|
export interface IAccessApiResponse<T> {
|
||||||
|
/** Code (SUCCESS, etc.) */
|
||||||
|
code: string;
|
||||||
|
/** Message */
|
||||||
|
msg?: string;
|
||||||
|
/** Data payload */
|
||||||
|
data: T;
|
||||||
|
/** Pagination info */
|
||||||
|
pagination?: {
|
||||||
|
page?: number;
|
||||||
|
page_size?: number;
|
||||||
|
total?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
75
ts/interfaces/common.ts
Normal file
75
ts/interfaces/common.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Common interfaces shared across all UniFi APIs
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard UniFi API response wrapper
|
||||||
|
*/
|
||||||
|
export interface IUnifiApiResponse<T> {
|
||||||
|
meta: {
|
||||||
|
rc: 'ok' | 'error';
|
||||||
|
msg?: string;
|
||||||
|
};
|
||||||
|
data: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for UniFi Account (Site Manager cloud API)
|
||||||
|
*/
|
||||||
|
export interface IUnifiAccountOptions {
|
||||||
|
/** API key from ui.com */
|
||||||
|
apiKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for UniFi Controller (Network Controller local API)
|
||||||
|
* Supports either API key auth OR username/password session auth
|
||||||
|
*/
|
||||||
|
export interface IUnifiControllerOptions {
|
||||||
|
/** Controller host (IP or hostname) */
|
||||||
|
host: string;
|
||||||
|
/** API key for authentication (preferred) */
|
||||||
|
apiKey?: string;
|
||||||
|
/** Username for session authentication */
|
||||||
|
username?: string;
|
||||||
|
/** Password for session authentication */
|
||||||
|
password?: string;
|
||||||
|
/** Controller type - affects API paths */
|
||||||
|
controllerType?: 'unifi-os' | 'udm-pro' | 'standalone';
|
||||||
|
/** Whether to verify SSL certificates (default: false for self-signed) */
|
||||||
|
verifySsl?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for UniFi Protect (NVR local API)
|
||||||
|
* Supports either API key auth OR username/password session auth
|
||||||
|
*/
|
||||||
|
export interface IUnifiProtectOptions {
|
||||||
|
/** Protect host (IP or hostname) */
|
||||||
|
host: string;
|
||||||
|
/** API key for authentication (preferred) */
|
||||||
|
apiKey?: string;
|
||||||
|
/** Username for session authentication */
|
||||||
|
username?: string;
|
||||||
|
/** Password for session authentication */
|
||||||
|
password?: string;
|
||||||
|
/** Whether to verify SSL certificates (default: false for self-signed) */
|
||||||
|
verifySsl?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for UniFi Access (Access Controller local API)
|
||||||
|
*/
|
||||||
|
export interface IUnifiAccessOptions {
|
||||||
|
/** Access host (IP or hostname) */
|
||||||
|
host: string;
|
||||||
|
/** Bearer token for authentication */
|
||||||
|
token: string;
|
||||||
|
/** Whether to verify SSL certificates (default: false for self-signed) */
|
||||||
|
verifySsl?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request method types
|
||||||
|
*/
|
||||||
|
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||||
9
ts/interfaces/index.ts
Normal file
9
ts/interfaces/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Re-export all interfaces
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './common.js';
|
||||||
|
export * from './sitemanager.js';
|
||||||
|
export * from './network.js';
|
||||||
|
export * from './protect.js';
|
||||||
|
export * from './access.js';
|
||||||
272
ts/interfaces/network.ts
Normal file
272
ts/interfaces/network.ts
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
/**
|
||||||
|
* Network Controller API interfaces
|
||||||
|
* Base URL: https://{host}/api or https://{host}/proxy/network/api
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network site
|
||||||
|
*/
|
||||||
|
export interface INetworkSite {
|
||||||
|
/** Site ID (e.g., 'default') */
|
||||||
|
_id: string;
|
||||||
|
/** Site name */
|
||||||
|
name: string;
|
||||||
|
/** Site description */
|
||||||
|
desc?: string;
|
||||||
|
/** Whether anonymous ID is enabled */
|
||||||
|
anonymous_id?: string;
|
||||||
|
/** Role for the site */
|
||||||
|
role?: string;
|
||||||
|
/** Attribute for hidden ID */
|
||||||
|
attr_hidden_id?: string;
|
||||||
|
/** Attribute for hidden and no delete */
|
||||||
|
attr_no_delete?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network device (switch, AP, gateway, etc.)
|
||||||
|
*/
|
||||||
|
export interface INetworkDevice {
|
||||||
|
/** Device ID */
|
||||||
|
_id: string;
|
||||||
|
/** Device MAC address */
|
||||||
|
mac: string;
|
||||||
|
/** Device model */
|
||||||
|
model: string;
|
||||||
|
/** Device type (ugw, usw, uap, etc.) */
|
||||||
|
type: string;
|
||||||
|
/** Device name */
|
||||||
|
name?: string;
|
||||||
|
/** Site ID */
|
||||||
|
site_id: string;
|
||||||
|
/** Whether device is adopted */
|
||||||
|
adopted: boolean;
|
||||||
|
/** Device IP address */
|
||||||
|
ip: string;
|
||||||
|
/** Device state (0=offline, 1=connected, etc.) */
|
||||||
|
state: number;
|
||||||
|
/** Serial number */
|
||||||
|
serial?: string;
|
||||||
|
/** Firmware version */
|
||||||
|
version?: string;
|
||||||
|
/** Uptime in seconds */
|
||||||
|
uptime?: number;
|
||||||
|
/** Last seen timestamp */
|
||||||
|
last_seen?: number;
|
||||||
|
/** Whether device is upgradable */
|
||||||
|
upgradable?: boolean;
|
||||||
|
/** Available upgrade version */
|
||||||
|
upgrade_to_firmware?: string;
|
||||||
|
/** Device configuration */
|
||||||
|
config_network?: {
|
||||||
|
type?: string;
|
||||||
|
ip?: string;
|
||||||
|
};
|
||||||
|
/** Device ethernet table */
|
||||||
|
ethernet_table?: Array<{
|
||||||
|
name: string;
|
||||||
|
mac: string;
|
||||||
|
num_port?: number;
|
||||||
|
}>;
|
||||||
|
/** Port overrides configuration */
|
||||||
|
port_overrides?: Array<{
|
||||||
|
port_idx: number;
|
||||||
|
name?: string;
|
||||||
|
poe_mode?: string;
|
||||||
|
}>;
|
||||||
|
/** System stats */
|
||||||
|
sys_stats?: {
|
||||||
|
loadavg_1?: number;
|
||||||
|
loadavg_5?: number;
|
||||||
|
loadavg_15?: number;
|
||||||
|
mem_total?: number;
|
||||||
|
mem_used?: number;
|
||||||
|
};
|
||||||
|
/** LED override */
|
||||||
|
led_override?: string;
|
||||||
|
/** LED override color */
|
||||||
|
led_override_color?: string;
|
||||||
|
/** LED override brightness */
|
||||||
|
led_override_color_brightness?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network client (connected device)
|
||||||
|
*/
|
||||||
|
export interface INetworkClient {
|
||||||
|
/** Client ID */
|
||||||
|
_id: string;
|
||||||
|
/** MAC address */
|
||||||
|
mac: string;
|
||||||
|
/** Site ID */
|
||||||
|
site_id: string;
|
||||||
|
/** Whether client is authorized (for guest network) */
|
||||||
|
is_guest?: boolean;
|
||||||
|
/** Whether client is wired */
|
||||||
|
is_wired: boolean;
|
||||||
|
/** First seen timestamp */
|
||||||
|
first_seen?: number;
|
||||||
|
/** Last seen timestamp */
|
||||||
|
last_seen?: number;
|
||||||
|
/** Hostname */
|
||||||
|
hostname?: string;
|
||||||
|
/** Client name (user-assigned) */
|
||||||
|
name?: string;
|
||||||
|
/** Client IP address */
|
||||||
|
ip?: string;
|
||||||
|
/** Network ID */
|
||||||
|
network_id?: string;
|
||||||
|
/** Uplink MAC (AP or switch) */
|
||||||
|
uplink_mac?: string;
|
||||||
|
/** Connected AP name */
|
||||||
|
ap_name?: string;
|
||||||
|
/** SSID if wireless */
|
||||||
|
essid?: string;
|
||||||
|
/** BSSID if wireless */
|
||||||
|
bssid?: string;
|
||||||
|
/** Channel if wireless */
|
||||||
|
channel?: number;
|
||||||
|
/** Radio protocol (ng, na, ac, ax) */
|
||||||
|
radio_proto?: string;
|
||||||
|
/** Signal strength */
|
||||||
|
signal?: number;
|
||||||
|
/** TX rate */
|
||||||
|
tx_rate?: number;
|
||||||
|
/** RX rate */
|
||||||
|
rx_rate?: number;
|
||||||
|
/** TX bytes */
|
||||||
|
tx_bytes?: number;
|
||||||
|
/** RX bytes */
|
||||||
|
rx_bytes?: number;
|
||||||
|
/** TX packets */
|
||||||
|
tx_packets?: number;
|
||||||
|
/** RX packets */
|
||||||
|
rx_packets?: number;
|
||||||
|
/** Connected switch port */
|
||||||
|
sw_port?: number;
|
||||||
|
/** User group ID */
|
||||||
|
usergroup_id?: string;
|
||||||
|
/** OUI (device manufacturer) */
|
||||||
|
oui?: string;
|
||||||
|
/** Noted status */
|
||||||
|
noted?: boolean;
|
||||||
|
/** User ID if fixed IP */
|
||||||
|
user_id?: string;
|
||||||
|
/** Fingerprint data */
|
||||||
|
fingerprint_source?: number;
|
||||||
|
/** Device fingerprint */
|
||||||
|
dev_cat?: number;
|
||||||
|
dev_family?: number;
|
||||||
|
dev_vendor?: number;
|
||||||
|
dev_id?: number;
|
||||||
|
/** OS name */
|
||||||
|
os_name?: number;
|
||||||
|
/** Satisfaction score */
|
||||||
|
satisfaction?: number;
|
||||||
|
/** Anomalies count */
|
||||||
|
anomalies?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network WLAN configuration
|
||||||
|
*/
|
||||||
|
export interface INetworkWlan {
|
||||||
|
/** WLAN ID */
|
||||||
|
_id: string;
|
||||||
|
/** WLAN name */
|
||||||
|
name: string;
|
||||||
|
/** Site ID */
|
||||||
|
site_id: string;
|
||||||
|
/** SSID */
|
||||||
|
x_passphrase?: string;
|
||||||
|
/** Whether WLAN is enabled */
|
||||||
|
enabled: boolean;
|
||||||
|
/** Security mode */
|
||||||
|
security?: string;
|
||||||
|
/** WPA mode */
|
||||||
|
wpa_mode?: string;
|
||||||
|
/** WPA encryption */
|
||||||
|
wpa_enc?: string;
|
||||||
|
/** VLAN ID */
|
||||||
|
networkconf_id?: string;
|
||||||
|
/** User group ID */
|
||||||
|
usergroup_id?: string;
|
||||||
|
/** Whether hidden */
|
||||||
|
hide_ssid?: boolean;
|
||||||
|
/** Whether PMF is enabled */
|
||||||
|
pmf_mode?: string;
|
||||||
|
/** Group rekey interval */
|
||||||
|
group_rekey?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network configuration (VLAN/subnet)
|
||||||
|
*/
|
||||||
|
export interface INetworkConfig {
|
||||||
|
/** Config ID */
|
||||||
|
_id: string;
|
||||||
|
/** Name */
|
||||||
|
name: string;
|
||||||
|
/** Site ID */
|
||||||
|
site_id: string;
|
||||||
|
/** Purpose (corporate, guest, wan, etc.) */
|
||||||
|
purpose: string;
|
||||||
|
/** VLAN ID */
|
||||||
|
vlan?: number;
|
||||||
|
/** VLAN enabled */
|
||||||
|
vlan_enabled?: boolean;
|
||||||
|
/** Subnet */
|
||||||
|
ip_subnet?: string;
|
||||||
|
/** DHCP enabled */
|
||||||
|
dhcpd_enabled?: boolean;
|
||||||
|
/** DHCP start */
|
||||||
|
dhcpd_start?: string;
|
||||||
|
/** DHCP stop */
|
||||||
|
dhcpd_stop?: string;
|
||||||
|
/** Domain name */
|
||||||
|
domain_name?: string;
|
||||||
|
/** Whether this is the default network */
|
||||||
|
is_nat?: boolean;
|
||||||
|
/** Network group */
|
||||||
|
networkgroup?: string;
|
||||||
|
/** IGMP snooping */
|
||||||
|
igmp_snooping?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device port configuration
|
||||||
|
*/
|
||||||
|
export interface IPortConfig {
|
||||||
|
port_idx: number;
|
||||||
|
name?: string;
|
||||||
|
poe_mode?: string;
|
||||||
|
port_poe?: boolean;
|
||||||
|
portconf_id?: string;
|
||||||
|
speed_caps?: number;
|
||||||
|
op_mode?: string;
|
||||||
|
autoneg?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth response from controller
|
||||||
|
*/
|
||||||
|
export interface INetworkAuthResponse {
|
||||||
|
/** Response code */
|
||||||
|
rc: string;
|
||||||
|
/** Session token/unique ID */
|
||||||
|
unique_id?: string;
|
||||||
|
/** First name */
|
||||||
|
first_name?: string;
|
||||||
|
/** Last name */
|
||||||
|
last_name?: string;
|
||||||
|
/** Full name */
|
||||||
|
full_name?: string;
|
||||||
|
/** Email */
|
||||||
|
email?: string;
|
||||||
|
/** Is super admin */
|
||||||
|
is_super?: boolean;
|
||||||
|
/** Device ID */
|
||||||
|
device_id?: string;
|
||||||
|
/** UI settings */
|
||||||
|
ui_settings?: Record<string, unknown>;
|
||||||
|
}
|
||||||
564
ts/interfaces/protect.ts
Normal file
564
ts/interfaces/protect.ts
Normal file
@@ -0,0 +1,564 @@
|
|||||||
|
/**
|
||||||
|
* Protect API interfaces
|
||||||
|
* Base URL: https://{host}/proxy/protect/api
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protect bootstrap response containing system configuration
|
||||||
|
*/
|
||||||
|
export interface IProtectBootstrap {
|
||||||
|
/** Auth user info */
|
||||||
|
authUser?: IProtectUser;
|
||||||
|
/** Access key */
|
||||||
|
accessKey?: string;
|
||||||
|
/** Cameras list */
|
||||||
|
cameras: IProtectCamera[];
|
||||||
|
/** Users list */
|
||||||
|
users?: IProtectUser[];
|
||||||
|
/** Groups list */
|
||||||
|
groups?: IProtectGroup[];
|
||||||
|
/** Liveviews */
|
||||||
|
liveviews?: IProtectLiveview[];
|
||||||
|
/** Viewers */
|
||||||
|
viewers?: IProtectViewer[];
|
||||||
|
/** Lights */
|
||||||
|
lights?: IProtectLight[];
|
||||||
|
/** Bridges */
|
||||||
|
bridges?: IProtectBridge[];
|
||||||
|
/** Sensors */
|
||||||
|
sensors?: IProtectSensor[];
|
||||||
|
/** Doorbells */
|
||||||
|
doorbells?: IProtectDoorbell[];
|
||||||
|
/** Chimes */
|
||||||
|
chimes?: IProtectChime[];
|
||||||
|
/** NVR info */
|
||||||
|
nvr: IProtectNvr;
|
||||||
|
/** Last update ID */
|
||||||
|
lastUpdateId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protect camera
|
||||||
|
*/
|
||||||
|
export interface IProtectCamera {
|
||||||
|
/** Camera ID */
|
||||||
|
id: string;
|
||||||
|
/** MAC address */
|
||||||
|
mac: string;
|
||||||
|
/** Host address */
|
||||||
|
host: string;
|
||||||
|
/** Camera name */
|
||||||
|
name: string;
|
||||||
|
/** Camera type/model */
|
||||||
|
type: string;
|
||||||
|
/** Model key */
|
||||||
|
modelKey?: string;
|
||||||
|
/** Camera state */
|
||||||
|
state: 'CONNECTED' | 'DISCONNECTED' | 'CONNECTING' | 'ADOPTING' | 'MANAGED';
|
||||||
|
/** Hardware revision */
|
||||||
|
hardwareRevision?: string;
|
||||||
|
/** Firmware version */
|
||||||
|
firmwareVersion?: string;
|
||||||
|
/** Firmware build */
|
||||||
|
firmwareBuild?: string;
|
||||||
|
/** Whether camera is updating */
|
||||||
|
isUpdating?: boolean;
|
||||||
|
/** Whether camera is adopting */
|
||||||
|
isAdopting?: boolean;
|
||||||
|
/** Whether camera is managed */
|
||||||
|
isManaged?: boolean;
|
||||||
|
/** Whether camera is connected */
|
||||||
|
isConnected?: boolean;
|
||||||
|
/** Whether recording is enabled */
|
||||||
|
isRecording?: boolean;
|
||||||
|
/** Whether motion detection is enabled */
|
||||||
|
isMotionDetected?: boolean;
|
||||||
|
/** Whether camera is dark (IR mode) */
|
||||||
|
isDark?: boolean;
|
||||||
|
/** Recording settings */
|
||||||
|
recordingSettings?: IProtectRecordingSettings;
|
||||||
|
/** Smart detect settings */
|
||||||
|
smartDetectSettings?: IProtectSmartDetectSettings;
|
||||||
|
/** ISP settings (image settings) */
|
||||||
|
ispSettings?: IProtectIspSettings;
|
||||||
|
/** Microphone settings */
|
||||||
|
micVolume?: number;
|
||||||
|
/** Speaker settings */
|
||||||
|
speakerVolume?: number;
|
||||||
|
/** Last motion timestamp */
|
||||||
|
lastMotion?: number;
|
||||||
|
/** Last ring timestamp (for doorbells) */
|
||||||
|
lastRing?: number;
|
||||||
|
/** Uptime */
|
||||||
|
uptime?: number;
|
||||||
|
/** Connected since */
|
||||||
|
connectedSince?: number;
|
||||||
|
/** Up since */
|
||||||
|
upSince?: number;
|
||||||
|
/** Last seen */
|
||||||
|
lastSeen?: number;
|
||||||
|
/** Channels info */
|
||||||
|
channels?: IProtectCameraChannel[];
|
||||||
|
/** Feature flags */
|
||||||
|
featureFlags?: IProtectFeatureFlags;
|
||||||
|
/** Stats */
|
||||||
|
stats?: IProtectCameraStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Camera channel configuration
|
||||||
|
*/
|
||||||
|
export interface IProtectCameraChannel {
|
||||||
|
/** Channel ID */
|
||||||
|
id: number;
|
||||||
|
/** Video mode */
|
||||||
|
videoMode?: string;
|
||||||
|
/** Enabled */
|
||||||
|
enabled: boolean;
|
||||||
|
/** FPS mode */
|
||||||
|
fpsValues?: number[];
|
||||||
|
/** Is RTSP enabled */
|
||||||
|
isRtspEnabled?: boolean;
|
||||||
|
/** RTSP alias */
|
||||||
|
rtspAlias?: string;
|
||||||
|
/** Width */
|
||||||
|
width?: number;
|
||||||
|
/** Height */
|
||||||
|
height?: number;
|
||||||
|
/** FPS */
|
||||||
|
fps?: number;
|
||||||
|
/** Bitrate */
|
||||||
|
bitrate?: number;
|
||||||
|
/** Min bitrate */
|
||||||
|
minBitrate?: number;
|
||||||
|
/** Max bitrate */
|
||||||
|
maxBitrate?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recording settings
|
||||||
|
*/
|
||||||
|
export interface IProtectRecordingSettings {
|
||||||
|
/** Pre-padding seconds */
|
||||||
|
prePaddingSecs?: number;
|
||||||
|
/** Post-padding seconds */
|
||||||
|
postPaddingSecs?: number;
|
||||||
|
/** Min motion event trigger */
|
||||||
|
minMotionEventTrigger?: number;
|
||||||
|
/** End motion event delay */
|
||||||
|
endMotionEventDelay?: number;
|
||||||
|
/** Suppress illumination surge */
|
||||||
|
suppressIlluminationSurge?: boolean;
|
||||||
|
/** Mode */
|
||||||
|
mode?: 'always' | 'detections' | 'never' | 'schedule';
|
||||||
|
/** Enable PIR timelapse */
|
||||||
|
enablePirTimelapse?: boolean;
|
||||||
|
/** Use new motion algorithm */
|
||||||
|
useNewMotionAlgorithm?: boolean;
|
||||||
|
/** In schedule mode */
|
||||||
|
inScheduleMode?: string;
|
||||||
|
/** Out schedule mode */
|
||||||
|
outScheduleMode?: string;
|
||||||
|
/** Geofencing */
|
||||||
|
geofencing?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smart detect settings
|
||||||
|
*/
|
||||||
|
export interface IProtectSmartDetectSettings {
|
||||||
|
/** Object types to detect */
|
||||||
|
objectTypes?: string[];
|
||||||
|
/** Audio types to detect */
|
||||||
|
audioTypes?: string[];
|
||||||
|
/** Auto tracking object types */
|
||||||
|
autoTrackingObjectTypes?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ISP (Image Signal Processor) settings
|
||||||
|
*/
|
||||||
|
export interface IProtectIspSettings {
|
||||||
|
/** AE mode */
|
||||||
|
aeMode?: string;
|
||||||
|
/** IR LED mode */
|
||||||
|
irLedMode?: string;
|
||||||
|
/** IR LED level */
|
||||||
|
irLedLevel?: number;
|
||||||
|
/** WDR */
|
||||||
|
wdr?: number;
|
||||||
|
/** ICR sensitivity */
|
||||||
|
icrSensitivity?: number;
|
||||||
|
/** Brightness */
|
||||||
|
brightness?: number;
|
||||||
|
/** Contrast */
|
||||||
|
contrast?: number;
|
||||||
|
/** Hue */
|
||||||
|
hue?: number;
|
||||||
|
/** Saturation */
|
||||||
|
saturation?: number;
|
||||||
|
/** Sharpness */
|
||||||
|
sharpness?: number;
|
||||||
|
/** Denoise */
|
||||||
|
denoise?: number;
|
||||||
|
/** Is flip enabled */
|
||||||
|
isFlippedVertical?: boolean;
|
||||||
|
/** Is mirror enabled */
|
||||||
|
isFlippedHorizontal?: boolean;
|
||||||
|
/** Is auto rotate enabled */
|
||||||
|
isAutoRotateEnabled?: boolean;
|
||||||
|
/** HDR mode */
|
||||||
|
hdrMode?: string;
|
||||||
|
/** Is color night vision enabled */
|
||||||
|
isColorNightVisionEnabled?: boolean;
|
||||||
|
/** Spotlight duration */
|
||||||
|
spotlightDuration?: number;
|
||||||
|
/** Focus mode */
|
||||||
|
focusMode?: string;
|
||||||
|
/** Focus position */
|
||||||
|
focusPosition?: number;
|
||||||
|
/** Zoom position */
|
||||||
|
zoomPosition?: number;
|
||||||
|
/** Touch focus X */
|
||||||
|
touchFocusX?: number;
|
||||||
|
/** Touch focus Y */
|
||||||
|
touchFocusY?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Camera feature flags
|
||||||
|
*/
|
||||||
|
export interface IProtectFeatureFlags {
|
||||||
|
/** Can adjust IR LED level */
|
||||||
|
canAdjustIrLedLevel?: boolean;
|
||||||
|
/** Has chime */
|
||||||
|
hasChime?: boolean;
|
||||||
|
/** Has flash */
|
||||||
|
hasFlash?: boolean;
|
||||||
|
/** Has HDR */
|
||||||
|
hasHdr?: boolean;
|
||||||
|
/** Has IR LED */
|
||||||
|
hasIrLed?: boolean;
|
||||||
|
/** Has LCD screen */
|
||||||
|
hasLcdScreen?: boolean;
|
||||||
|
/** Has LED status */
|
||||||
|
hasLedStatus?: boolean;
|
||||||
|
/** Has line in */
|
||||||
|
hasLineIn?: boolean;
|
||||||
|
/** Has mic */
|
||||||
|
hasMic?: boolean;
|
||||||
|
/** Has privacy mask */
|
||||||
|
hasPrivacyMask?: boolean;
|
||||||
|
/** Has RTSP */
|
||||||
|
hasRtsp?: boolean;
|
||||||
|
/** Has SD card */
|
||||||
|
hasSdCard?: boolean;
|
||||||
|
/** Has smart detect */
|
||||||
|
hasSmartDetect?: boolean;
|
||||||
|
/** Has speaker */
|
||||||
|
hasSpeaker?: boolean;
|
||||||
|
/** Has WiFi */
|
||||||
|
hasWifi?: boolean;
|
||||||
|
/** Video modes */
|
||||||
|
videoModes?: string[];
|
||||||
|
/** Privacy mask capability */
|
||||||
|
privacyMaskCapability?: {
|
||||||
|
maxMasks?: number;
|
||||||
|
rectangleOnly?: boolean;
|
||||||
|
};
|
||||||
|
/** Smart detect types */
|
||||||
|
smartDetectTypes?: string[];
|
||||||
|
/** Smart detect audio types */
|
||||||
|
smartDetectAudioTypes?: string[];
|
||||||
|
/** Motion algorithms */
|
||||||
|
motionAlgorithms?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Camera stats
|
||||||
|
*/
|
||||||
|
export interface IProtectCameraStats {
|
||||||
|
/** RX bytes */
|
||||||
|
rxBytes?: number;
|
||||||
|
/** TX bytes */
|
||||||
|
txBytes?: number;
|
||||||
|
/** WiFi quality */
|
||||||
|
wifiQuality?: number;
|
||||||
|
/** WiFi strength */
|
||||||
|
wifiStrength?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protect user
|
||||||
|
*/
|
||||||
|
export interface IProtectUser {
|
||||||
|
/** User ID */
|
||||||
|
id: string;
|
||||||
|
/** Is owner */
|
||||||
|
isOwner?: boolean;
|
||||||
|
/** Name */
|
||||||
|
name?: string;
|
||||||
|
/** Email */
|
||||||
|
email?: string;
|
||||||
|
/** Local username */
|
||||||
|
localUsername?: string;
|
||||||
|
/** Has accepted invite */
|
||||||
|
hasAcceptedInvite?: boolean;
|
||||||
|
/** All permissions */
|
||||||
|
allPermissions?: string[];
|
||||||
|
/** Model key */
|
||||||
|
modelKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protect group
|
||||||
|
*/
|
||||||
|
export interface IProtectGroup {
|
||||||
|
/** Group ID */
|
||||||
|
id: string;
|
||||||
|
/** Group name */
|
||||||
|
name: string;
|
||||||
|
/** Group type */
|
||||||
|
type?: string;
|
||||||
|
/** Model key */
|
||||||
|
modelKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protect liveview
|
||||||
|
*/
|
||||||
|
export interface IProtectLiveview {
|
||||||
|
/** Liveview ID */
|
||||||
|
id: string;
|
||||||
|
/** Name */
|
||||||
|
name: string;
|
||||||
|
/** Is default */
|
||||||
|
isDefault?: boolean;
|
||||||
|
/** Layout */
|
||||||
|
layout?: number;
|
||||||
|
/** Model key */
|
||||||
|
modelKey?: string;
|
||||||
|
/** Slots */
|
||||||
|
slots?: IProtectLiveviewSlot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protect liveview slot
|
||||||
|
*/
|
||||||
|
export interface IProtectLiveviewSlot {
|
||||||
|
/** Camera IDs */
|
||||||
|
cameras?: string[];
|
||||||
|
/** Cycle mode */
|
||||||
|
cycleMode?: string;
|
||||||
|
/** Cycle interval */
|
||||||
|
cycleInterval?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protect viewer
|
||||||
|
*/
|
||||||
|
export interface IProtectViewer {
|
||||||
|
/** Viewer ID */
|
||||||
|
id: string;
|
||||||
|
/** Name */
|
||||||
|
name?: string;
|
||||||
|
/** Model key */
|
||||||
|
modelKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protect light
|
||||||
|
*/
|
||||||
|
export interface IProtectLight {
|
||||||
|
/** Light ID */
|
||||||
|
id: string;
|
||||||
|
/** MAC */
|
||||||
|
mac: string;
|
||||||
|
/** Name */
|
||||||
|
name: string;
|
||||||
|
/** Type */
|
||||||
|
type: string;
|
||||||
|
/** State */
|
||||||
|
state: string;
|
||||||
|
/** Is light on */
|
||||||
|
isLightOn?: boolean;
|
||||||
|
/** Light device settings */
|
||||||
|
lightDeviceSettings?: {
|
||||||
|
ledLevel?: number;
|
||||||
|
luxSensitivity?: string;
|
||||||
|
pirDuration?: number;
|
||||||
|
pirSensitivity?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protect bridge
|
||||||
|
*/
|
||||||
|
export interface IProtectBridge {
|
||||||
|
/** Bridge ID */
|
||||||
|
id: string;
|
||||||
|
/** MAC */
|
||||||
|
mac: string;
|
||||||
|
/** Name */
|
||||||
|
name: string;
|
||||||
|
/** Type */
|
||||||
|
type: string;
|
||||||
|
/** State */
|
||||||
|
state: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protect sensor
|
||||||
|
*/
|
||||||
|
export interface IProtectSensor {
|
||||||
|
/** Sensor ID */
|
||||||
|
id: string;
|
||||||
|
/** MAC */
|
||||||
|
mac: string;
|
||||||
|
/** Name */
|
||||||
|
name: string;
|
||||||
|
/** Type */
|
||||||
|
type: string;
|
||||||
|
/** State */
|
||||||
|
state: string;
|
||||||
|
/** Battery status */
|
||||||
|
batteryStatus?: {
|
||||||
|
percentage?: number;
|
||||||
|
isLow?: boolean;
|
||||||
|
};
|
||||||
|
/** Mount type */
|
||||||
|
mountType?: string;
|
||||||
|
/** Is motion detected */
|
||||||
|
isMotionDetected?: boolean;
|
||||||
|
/** Is opened */
|
||||||
|
isOpened?: boolean;
|
||||||
|
/** Humidity */
|
||||||
|
humidity?: number;
|
||||||
|
/** Temperature */
|
||||||
|
temperature?: number;
|
||||||
|
/** Light */
|
||||||
|
light?: number;
|
||||||
|
/** Alarm triggered type */
|
||||||
|
alarmTriggeredType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protect doorbell (extends camera)
|
||||||
|
*/
|
||||||
|
export interface IProtectDoorbell extends IProtectCamera {
|
||||||
|
/** LCD message */
|
||||||
|
lcdMessage?: {
|
||||||
|
text?: string;
|
||||||
|
resetAt?: number;
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
/** Chime duration */
|
||||||
|
chimeDuration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protect chime
|
||||||
|
*/
|
||||||
|
export interface IProtectChime {
|
||||||
|
/** Chime ID */
|
||||||
|
id: string;
|
||||||
|
/** MAC */
|
||||||
|
mac: string;
|
||||||
|
/** Name */
|
||||||
|
name: string;
|
||||||
|
/** Type */
|
||||||
|
type: string;
|
||||||
|
/** State */
|
||||||
|
state: string;
|
||||||
|
/** Is paired with doorbell */
|
||||||
|
isPaired?: boolean;
|
||||||
|
/** Volume */
|
||||||
|
volume?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protect NVR info
|
||||||
|
*/
|
||||||
|
export interface IProtectNvr {
|
||||||
|
/** NVR ID */
|
||||||
|
id: string;
|
||||||
|
/** MAC */
|
||||||
|
mac: string;
|
||||||
|
/** Host */
|
||||||
|
host: string;
|
||||||
|
/** Name */
|
||||||
|
name: string;
|
||||||
|
/** Type */
|
||||||
|
type: string;
|
||||||
|
/** Is connected to cloud */
|
||||||
|
isConnectedToCloud?: boolean;
|
||||||
|
/** Firmware version */
|
||||||
|
firmwareVersion?: string;
|
||||||
|
/** Hardware */
|
||||||
|
hardware?: {
|
||||||
|
shortname?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
/** Uptime */
|
||||||
|
uptime?: number;
|
||||||
|
/** Last seen */
|
||||||
|
lastSeen?: number;
|
||||||
|
/** Is recording disabled */
|
||||||
|
isRecordingDisabled?: boolean;
|
||||||
|
/** Is recording motion only */
|
||||||
|
isRecordingMotionOnly?: boolean;
|
||||||
|
/** Storage info */
|
||||||
|
storageInfo?: {
|
||||||
|
totalSize?: number;
|
||||||
|
totalSpaceUsed?: number;
|
||||||
|
devices?: Array<{
|
||||||
|
model?: string;
|
||||||
|
size?: number;
|
||||||
|
healthy?: boolean;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
/** Timezone */
|
||||||
|
timezone?: string;
|
||||||
|
/** Version */
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth response from Protect
|
||||||
|
*/
|
||||||
|
export interface IProtectAuthResponse {
|
||||||
|
/** CSRF token in response */
|
||||||
|
csrfToken?: string;
|
||||||
|
/** User info */
|
||||||
|
user?: IProtectUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Motion event from Protect
|
||||||
|
*/
|
||||||
|
export interface IProtectMotionEvent {
|
||||||
|
/** Event ID */
|
||||||
|
id: string;
|
||||||
|
/** Event type */
|
||||||
|
type: 'motion' | 'ring' | 'smartDetectZone' | 'smartAudioDetect';
|
||||||
|
/** Start timestamp */
|
||||||
|
start: number;
|
||||||
|
/** End timestamp */
|
||||||
|
end?: number;
|
||||||
|
/** Score (confidence) */
|
||||||
|
score?: number;
|
||||||
|
/** Smart detect types */
|
||||||
|
smartDetectTypes?: string[];
|
||||||
|
/** Smart detect events */
|
||||||
|
smartDetectEvents?: string[];
|
||||||
|
/** Camera ID */
|
||||||
|
camera?: string;
|
||||||
|
/** Partition (for storage) */
|
||||||
|
partition?: string;
|
||||||
|
/** Model key */
|
||||||
|
modelKey?: string;
|
||||||
|
/** Thumbnail ID */
|
||||||
|
thumbnail?: string;
|
||||||
|
/** Has heatmap */
|
||||||
|
heatmap?: string;
|
||||||
|
}
|
||||||
78
ts/interfaces/sitemanager.ts
Normal file
78
ts/interfaces/sitemanager.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Site Manager (Cloud) API interfaces
|
||||||
|
* Base URL: https://api.ui.com/v1
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Site from Site Manager API
|
||||||
|
*/
|
||||||
|
export interface IUnifiSite {
|
||||||
|
/** Unique site ID */
|
||||||
|
siteId: string;
|
||||||
|
/** Site name */
|
||||||
|
name: string;
|
||||||
|
/** Site description */
|
||||||
|
description?: string;
|
||||||
|
/** Whether this is the default site */
|
||||||
|
isDefault?: boolean;
|
||||||
|
/** Timezone for the site */
|
||||||
|
timezone?: string;
|
||||||
|
/** Site meta info */
|
||||||
|
meta?: {
|
||||||
|
/** Site type */
|
||||||
|
type?: string;
|
||||||
|
/** Site address */
|
||||||
|
address?: string;
|
||||||
|
};
|
||||||
|
/** Creation timestamp */
|
||||||
|
createdAt?: string;
|
||||||
|
/** Last modified timestamp */
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Host device from Site Manager API
|
||||||
|
*/
|
||||||
|
export interface IUnifiHost {
|
||||||
|
/** Unique host ID */
|
||||||
|
id: string;
|
||||||
|
/** Hardware UUID */
|
||||||
|
hardwareId?: string;
|
||||||
|
/** Host name */
|
||||||
|
name?: string;
|
||||||
|
/** Host type (e.g., 'udm-pro', 'cloud-key-gen2-plus') */
|
||||||
|
type?: string;
|
||||||
|
/** Firmware version */
|
||||||
|
firmwareVersion?: string;
|
||||||
|
/** Whether the host is online */
|
||||||
|
isOnline?: boolean;
|
||||||
|
/** IP address */
|
||||||
|
ipAddress?: string;
|
||||||
|
/** MAC address */
|
||||||
|
macAddress?: string;
|
||||||
|
/** Associated site ID */
|
||||||
|
siteId?: string;
|
||||||
|
/** Host status info */
|
||||||
|
status?: {
|
||||||
|
/** Connection state */
|
||||||
|
state?: string;
|
||||||
|
/** Last seen timestamp */
|
||||||
|
lastSeen?: string;
|
||||||
|
};
|
||||||
|
/** Features/applications running */
|
||||||
|
features?: string[];
|
||||||
|
/** Reported state from host */
|
||||||
|
reportedState?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Site Manager API list response
|
||||||
|
*/
|
||||||
|
export interface ISiteManagerListResponse<T> {
|
||||||
|
data: T[];
|
||||||
|
meta?: {
|
||||||
|
total?: number;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,6 +4,14 @@ import * as path from 'path';
|
|||||||
export { path };
|
export { path };
|
||||||
|
|
||||||
// @push.rocks scope
|
// @push.rocks scope
|
||||||
|
import * as smartlog from '@push.rocks/smartlog';
|
||||||
import * as smartpath from '@push.rocks/smartpath';
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
|
import * as smartrequest from '@push.rocks/smartrequest';
|
||||||
|
import * as smartstring from '@push.rocks/smartstring';
|
||||||
|
|
||||||
export { smartpath };
|
export { smartlog, smartpath, smartpromise, smartrequest, smartstring };
|
||||||
|
|
||||||
|
// Re-export smartrequest types
|
||||||
|
export type { IExtendedIncomingMessage } from '@push.rocks/smartrequest';
|
||||||
|
export { SmartRequestClient } from '@push.rocks/smartrequest';
|
||||||
|
|||||||
12
ts/unifi.logger.ts
Normal file
12
ts/unifi.logger.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
|
export const logger = new plugins.smartlog.Smartlog({
|
||||||
|
logContext: {
|
||||||
|
company: 'apiclient.xyz',
|
||||||
|
companyunit: 'unifi',
|
||||||
|
containerName: 'unifi-client',
|
||||||
|
environment: 'production',
|
||||||
|
runtime: 'node',
|
||||||
|
zone: 'api',
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user