Initial commit: Omada API TypeScript library
This commit is contained in:
0
.env.example
Normal file
0
.env.example
Normal file
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
46
NOTES.md
Normal file
46
NOTES.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Implementation Notes
|
||||||
|
|
||||||
|
## API Endpoint Compatibility
|
||||||
|
|
||||||
|
This library is built based on the Omada Controller REST API specification. However, the actual API endpoints may vary slightly between Omada Controller versions. The following adjustments may be needed:
|
||||||
|
|
||||||
|
### Authentication Endpoint
|
||||||
|
|
||||||
|
The authentication endpoint used is `/api/v2/login`. If your Omada Controller uses a different endpoint, you may need to adjust the `Authentication.ts` file.
|
||||||
|
|
||||||
|
Common alternatives:
|
||||||
|
- `/api/v2/oauth/token` (OAuth2 token endpoint)
|
||||||
|
- `/api/v1/login` (v1 API)
|
||||||
|
|
||||||
|
### API Path Structure
|
||||||
|
|
||||||
|
The library assumes the API follows REST conventions:
|
||||||
|
- `/sites/{siteId}/devices/{deviceId}`
|
||||||
|
- `/sites/{siteId}/vlans/{vlanId}`
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
If your Omada Controller uses a different path structure, you may need to adjust the service classes accordingly.
|
||||||
|
|
||||||
|
## SSL Certificate Handling
|
||||||
|
|
||||||
|
The library supports self-signed certificates by setting `verifySSL: false`. However, when using native `fetch` in Node.js 18+, SSL certificate handling may work differently than with `node-fetch`.
|
||||||
|
|
||||||
|
If you encounter SSL certificate issues:
|
||||||
|
|
||||||
|
1. **Option 1**: Use `node-fetch` package instead of native fetch
|
||||||
|
2. **Option 2**: Set `NODE_TLS_REJECT_UNAUTHORIZED=0` environment variable (development only)
|
||||||
|
3. **Option 3**: Install proper SSL certificates on the Omada Controller
|
||||||
|
|
||||||
|
## Testing with Actual Omada Controller
|
||||||
|
|
||||||
|
Before using in production, test with your actual Omada Controller to verify:
|
||||||
|
|
||||||
|
1. Authentication endpoint and request format
|
||||||
|
2. API response structure
|
||||||
|
3. Error code meanings
|
||||||
|
4. Required vs optional parameters
|
||||||
|
|
||||||
|
## Contributing Improvements
|
||||||
|
|
||||||
|
If you discover API endpoint differences or need additional features, please update the service classes accordingly.
|
||||||
|
|
||||||
188
README.md
Normal file
188
README.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# Omada API Library
|
||||||
|
|
||||||
|
TypeScript library for interacting with TP-Link Omada Controller REST API.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Type-safe API client with full TypeScript support
|
||||||
|
- OAuth2 authentication with automatic token refresh
|
||||||
|
- Support for all Omada devices:
|
||||||
|
- Routers (ER605, etc.)
|
||||||
|
- Switches (SG218R, etc.)
|
||||||
|
- Access Points (EAP)
|
||||||
|
- Complete device management (list, configure, reboot, adopt)
|
||||||
|
- Network configuration (VLANs, DHCP, routing)
|
||||||
|
- Firewall and NAT rule management
|
||||||
|
- Switch port configuration and PoE management
|
||||||
|
- Router WAN/LAN configuration
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Setup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { OmadaClient } from 'omada-api';
|
||||||
|
|
||||||
|
const client = new OmadaClient({
|
||||||
|
baseUrl: 'https://192.168.11.10:8043',
|
||||||
|
clientId: 'your-api-key',
|
||||||
|
clientSecret: 'your-api-secret',
|
||||||
|
siteId: 'your-site-id', // Optional, will auto-detect
|
||||||
|
verifySSL: false, // Set to true for production
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Device Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DevicesService, DeviceType } from 'omada-api';
|
||||||
|
|
||||||
|
const devicesService = new DevicesService(client);
|
||||||
|
|
||||||
|
// List all devices
|
||||||
|
const devices = await devicesService.listDevices();
|
||||||
|
|
||||||
|
// Get routers
|
||||||
|
const routers = await devicesService.getRouters();
|
||||||
|
|
||||||
|
// Get switches
|
||||||
|
const switches = await devicesService.getSwitches();
|
||||||
|
|
||||||
|
// Get device details
|
||||||
|
const device = await devicesService.getDevice('device-id');
|
||||||
|
|
||||||
|
// Reboot a device
|
||||||
|
await devicesService.rebootDevice('device-id');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { NetworksService } from 'omada-api';
|
||||||
|
|
||||||
|
const networksService = new NetworksService(client);
|
||||||
|
|
||||||
|
// List VLANs
|
||||||
|
const vlans = await networksService.listVLANs();
|
||||||
|
|
||||||
|
// Create a VLAN
|
||||||
|
const vlan = await networksService.createVLAN({
|
||||||
|
name: 'VLAN-100',
|
||||||
|
vlanId: 100,
|
||||||
|
subnet: '10.100.0.0/24',
|
||||||
|
gateway: '10.100.0.1',
|
||||||
|
dhcpEnable: true,
|
||||||
|
dhcpRangeStart: '10.100.0.100',
|
||||||
|
dhcpRangeEnd: '10.100.0.200',
|
||||||
|
dns1: '8.8.8.8',
|
||||||
|
dns2: '1.1.1.1',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Router Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { RouterService } from 'omada-api';
|
||||||
|
|
||||||
|
const routerService = new RouterService(client);
|
||||||
|
|
||||||
|
// Get WAN ports
|
||||||
|
const wanPorts = await routerService.getWANPorts('router-device-id');
|
||||||
|
|
||||||
|
// Configure WAN port
|
||||||
|
await routerService.configureWANPort('router-device-id', 1, {
|
||||||
|
connectionType: 'static',
|
||||||
|
ip: '192.168.1.100',
|
||||||
|
gateway: '192.168.1.1',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Switch Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { SwitchService } from 'omada-api';
|
||||||
|
|
||||||
|
const switchService = new SwitchService(client);
|
||||||
|
|
||||||
|
// Get switch ports
|
||||||
|
const ports = await switchService.getSwitchPorts('switch-device-id');
|
||||||
|
|
||||||
|
// Configure a port
|
||||||
|
await switchService.configurePort('switch-device-id', 1, {
|
||||||
|
enable: true,
|
||||||
|
name: 'Port 1',
|
||||||
|
vlanMode: 'access',
|
||||||
|
nativeVlanId: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Control PoE
|
||||||
|
await switchService.setPoE('switch-device-id', 1, true);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Firewall Rules
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { FirewallService } from 'omada-api';
|
||||||
|
|
||||||
|
const firewallService = new FirewallService(client);
|
||||||
|
|
||||||
|
// Create firewall rule
|
||||||
|
await firewallService.createFirewallRule({
|
||||||
|
name: 'Allow SSH',
|
||||||
|
enable: true,
|
||||||
|
action: 'allow',
|
||||||
|
protocol: 'tcp',
|
||||||
|
dstPort: '22',
|
||||||
|
direction: 'in',
|
||||||
|
priority: 100,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The library can be configured via environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OMADA_CONTROLLER_URL=https://192.168.11.10:8043
|
||||||
|
OMADA_API_KEY=your-api-key
|
||||||
|
OMADA_API_SECRET=your-api-secret
|
||||||
|
OMADA_SITE_ID=your-site-id # Optional
|
||||||
|
OMADA_VERIFY_SSL=false # Set to true for production
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The library provides specific error classes for different error scenarios:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
OmadaApiError,
|
||||||
|
OmadaAuthenticationError,
|
||||||
|
OmadaDeviceNotFoundError,
|
||||||
|
OmadaConfigurationError,
|
||||||
|
} from 'omada-api';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await devicesService.getDevice('device-id');
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof OmadaDeviceNotFoundError) {
|
||||||
|
console.error('Device not found');
|
||||||
|
} else if (error instanceof OmadaAuthenticationError) {
|
||||||
|
console.error('Authentication failed');
|
||||||
|
} else if (error instanceof OmadaApiError) {
|
||||||
|
console.error('API error:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
See the TypeScript type definitions for complete API documentation. All services and types are fully typed.
|
||||||
|
|
||||||
34
package.json
Normal file
34
package.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "omada-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TypeScript library for TP-Link Omada Controller REST API",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsc --watch",
|
||||||
|
"clean": "rm -rf dist"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"omada",
|
||||||
|
"tp-link",
|
||||||
|
"router",
|
||||||
|
"switch",
|
||||||
|
"api",
|
||||||
|
"network"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"typescript": "^5.9.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
139
src/auth/Authentication.ts
Normal file
139
src/auth/Authentication.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* OAuth2 authentication and token management for Omada Controller API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import https from 'https';
|
||||||
|
import { OmadaAuthenticationError, OmadaNetworkError } from '../errors/OmadaErrors.js';
|
||||||
|
|
||||||
|
export interface AuthConfig {
|
||||||
|
baseUrl: string;
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
verifySSL?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TokenCache {
|
||||||
|
token: string;
|
||||||
|
expiresAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages OAuth2 authentication and token lifecycle
|
||||||
|
*/
|
||||||
|
export class Authentication {
|
||||||
|
private config: AuthConfig;
|
||||||
|
private tokenCache: TokenCache | null = null;
|
||||||
|
private httpsAgent: https.Agent;
|
||||||
|
|
||||||
|
constructor(config: AuthConfig) {
|
||||||
|
this.config = {
|
||||||
|
verifySSL: true,
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create HTTPS agent with SSL verification control
|
||||||
|
this.httpsAgent = new https.Agent({
|
||||||
|
rejectUnauthorized: this.config.verifySSL ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a valid access token, refreshing if necessary
|
||||||
|
*/
|
||||||
|
async getAccessToken(): Promise<string> {
|
||||||
|
// Check if we have a valid cached token
|
||||||
|
if (this.tokenCache && this.tokenCache.expiresAt > Date.now() + 60000) {
|
||||||
|
// Token is still valid (with 1 minute buffer)
|
||||||
|
return this.tokenCache.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request new token
|
||||||
|
return this.requestToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a new access token from the Omada Controller
|
||||||
|
*/
|
||||||
|
private async requestToken(): Promise<string> {
|
||||||
|
const url = `${this.config.baseUrl}/api/v2/login`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: this.config.clientId,
|
||||||
|
password: this.config.clientSecret,
|
||||||
|
}),
|
||||||
|
// Note: Native fetch in Node.js 18+ doesn't support agent directly
|
||||||
|
// For SSL certificate handling, ensure verifySSL config is set correctly
|
||||||
|
// @ts-expect-error - agent may not be supported by all fetch implementations
|
||||||
|
agent: this.httpsAgent,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new OmadaAuthenticationError(
|
||||||
|
`Failed to authenticate: ${response.status} ${response.statusText} - ${errorText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json() as {
|
||||||
|
errorCode: number;
|
||||||
|
msg?: string;
|
||||||
|
result?: {
|
||||||
|
token?: string;
|
||||||
|
expiresIn?: number;
|
||||||
|
};
|
||||||
|
token?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.errorCode !== 0) {
|
||||||
|
throw new OmadaAuthenticationError(
|
||||||
|
`Authentication failed: ${data.msg || 'Unknown error'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Omada Controller returns token in result.token or directly as token
|
||||||
|
const token = data.result?.token || data.token;
|
||||||
|
|
||||||
|
if (!token || typeof token !== 'string') {
|
||||||
|
throw new OmadaAuthenticationError('No token received from server');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache token (default expiration: 1 hour, but we'll refresh after 50 minutes)
|
||||||
|
const expiresIn = data.result?.expiresIn || 3600;
|
||||||
|
this.tokenCache = {
|
||||||
|
token,
|
||||||
|
expiresAt: Date.now() + (expiresIn - 600) * 1000, // Refresh 10 minutes before expiry
|
||||||
|
};
|
||||||
|
|
||||||
|
return token;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof OmadaAuthenticationError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new OmadaNetworkError(
|
||||||
|
`Failed to connect to Omada Controller: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
error instanceof Error ? error : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cached token (force refresh on next request)
|
||||||
|
*/
|
||||||
|
clearToken(): void {
|
||||||
|
this.tokenCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we have a valid cached token
|
||||||
|
*/
|
||||||
|
hasValidToken(): boolean {
|
||||||
|
return this.tokenCache !== null && this.tokenCache.expiresAt > Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
182
src/client/OmadaClient.ts
Normal file
182
src/client/OmadaClient.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
/**
|
||||||
|
* Core API client for Omada Controller REST API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import https from 'https';
|
||||||
|
import { Authentication, AuthConfig } from '../auth/Authentication.js';
|
||||||
|
import {
|
||||||
|
OmadaApiError,
|
||||||
|
OmadaNetworkError,
|
||||||
|
OmadaDeviceNotFoundError,
|
||||||
|
OmadaConfigurationError,
|
||||||
|
} from '../errors/OmadaErrors.js';
|
||||||
|
import { ApiResponse, ApiRequestOptions, PaginatedResponse } from '../types/api.js';
|
||||||
|
|
||||||
|
export interface OmadaClientConfig extends AuthConfig {
|
||||||
|
siteId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main client class for interacting with Omada Controller API
|
||||||
|
*/
|
||||||
|
export class OmadaClient {
|
||||||
|
private auth: Authentication;
|
||||||
|
private config: OmadaClientConfig;
|
||||||
|
private httpsAgent: https.Agent;
|
||||||
|
private siteId: string | null = null;
|
||||||
|
|
||||||
|
constructor(config: OmadaClientConfig) {
|
||||||
|
this.config = config;
|
||||||
|
this.auth = new Authentication(config);
|
||||||
|
this.httpsAgent = new https.Agent({
|
||||||
|
rejectUnauthorized: config.verifySSL ?? true,
|
||||||
|
});
|
||||||
|
this.siteId = config.siteId || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current site ID (auto-detect if not set)
|
||||||
|
*/
|
||||||
|
async getSiteId(): Promise<string> {
|
||||||
|
if (this.siteId) {
|
||||||
|
return this.siteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect site ID by getting the default site
|
||||||
|
const sites = await this.request<Array<{ id: string; name: string }>>('GET', '/sites');
|
||||||
|
if (!sites || sites.length === 0) {
|
||||||
|
throw new OmadaApiError('No sites found in Omada Controller');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the first site as default
|
||||||
|
this.siteId = sites[0].id;
|
||||||
|
return this.siteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the site ID explicitly
|
||||||
|
*/
|
||||||
|
setSiteId(siteId: string): void {
|
||||||
|
this.siteId = siteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an authenticated API request
|
||||||
|
*/
|
||||||
|
async request<T = any>(
|
||||||
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
|
||||||
|
endpoint: string,
|
||||||
|
options: Omit<ApiRequestOptions, 'method'> = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const token = await this.auth.getAccessToken();
|
||||||
|
const url = this.buildUrl(endpoint, options.params);
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
...options.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||||
|
// Note: Native fetch in Node.js 18+ doesn't support agent directly
|
||||||
|
// For SSL certificate handling, ensure verifySSL config is set correctly
|
||||||
|
// @ts-expect-error - agent may not be supported by all fetch implementations
|
||||||
|
agent: this.httpsAgent,
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
let data: ApiResponse<T>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = JSON.parse(text);
|
||||||
|
} catch (parseError) {
|
||||||
|
throw new OmadaApiError(
|
||||||
|
`Invalid JSON response: ${text.substring(0, 200)}`,
|
||||||
|
response.status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new OmadaApiError(
|
||||||
|
data.msg || `HTTP ${response.status}: ${response.statusText}`,
|
||||||
|
response.status,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Omada API error code (0 = success)
|
||||||
|
if (data.errorCode !== 0) {
|
||||||
|
const errorMsg = data.msg || `API error code: ${data.errorCode}`;
|
||||||
|
|
||||||
|
// Handle specific error cases
|
||||||
|
if (data.errorCode === 10001) {
|
||||||
|
// Token expired or invalid
|
||||||
|
this.auth.clearToken();
|
||||||
|
throw new OmadaApiError('Authentication token expired', 401, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.errorCode === 10002 || data.errorCode === 10003) {
|
||||||
|
throw new OmadaDeviceNotFoundError(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.errorCode >= 10000 && data.errorCode < 20000) {
|
||||||
|
throw new OmadaConfigurationError(errorMsg, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new OmadaApiError(errorMsg, response.status, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.result as T;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof OmadaApiError || error instanceof OmadaDeviceNotFoundError || error instanceof OmadaConfigurationError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new OmadaNetworkError(
|
||||||
|
`Request failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
error instanceof Error ? error : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a paginated API request
|
||||||
|
*/
|
||||||
|
async requestPaginated<T = any>(
|
||||||
|
method: 'GET' | 'POST',
|
||||||
|
endpoint: string,
|
||||||
|
options: Omit<ApiRequestOptions, 'method'> = {}
|
||||||
|
): Promise<PaginatedResponse<T>> {
|
||||||
|
const result = await this.request<PaginatedResponse<T>>(method, endpoint, options);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build full URL with query parameters
|
||||||
|
*/
|
||||||
|
private buildUrl(endpoint: string, params?: Record<string, string | number | boolean>): string {
|
||||||
|
const baseUrl = this.config.baseUrl.replace(/\/$/, '');
|
||||||
|
let url = `${baseUrl}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
|
||||||
|
|
||||||
|
if (params && Object.keys(params).length > 0) {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
searchParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
url += `?${searchParams.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get authentication instance (for advanced use cases)
|
||||||
|
*/
|
||||||
|
getAuth(): Authentication {
|
||||||
|
return this.auth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
48
src/errors/OmadaErrors.ts
Normal file
48
src/errors/OmadaErrors.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Custom error classes for Omada API operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class OmadaApiError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public statusCode?: number,
|
||||||
|
public response?: any
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'OmadaApiError';
|
||||||
|
Object.setPrototypeOf(this, OmadaApiError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OmadaAuthenticationError extends OmadaApiError {
|
||||||
|
constructor(message: string = 'Authentication failed', response?: any) {
|
||||||
|
super(message, 401, response);
|
||||||
|
this.name = 'OmadaAuthenticationError';
|
||||||
|
Object.setPrototypeOf(this, OmadaAuthenticationError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OmadaDeviceNotFoundError extends OmadaApiError {
|
||||||
|
constructor(deviceId: string) {
|
||||||
|
super(`Device not found: ${deviceId}`, 404);
|
||||||
|
this.name = 'OmadaDeviceNotFoundError';
|
||||||
|
Object.setPrototypeOf(this, OmadaDeviceNotFoundError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OmadaConfigurationError extends OmadaApiError {
|
||||||
|
constructor(message: string, response?: any) {
|
||||||
|
super(`Configuration error: ${message}`, 400, response);
|
||||||
|
this.name = 'OmadaConfigurationError';
|
||||||
|
Object.setPrototypeOf(this, OmadaConfigurationError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OmadaNetworkError extends OmadaApiError {
|
||||||
|
constructor(message: string, originalError?: Error) {
|
||||||
|
super(`Network error: ${message}`, undefined, originalError);
|
||||||
|
this.name = 'OmadaNetworkError';
|
||||||
|
Object.setPrototypeOf(this, OmadaNetworkError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
32
src/index.ts
Normal file
32
src/index.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Omada API Library
|
||||||
|
*
|
||||||
|
* TypeScript library for interacting with TP-Link Omada Controller REST API
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { OmadaClient, type OmadaClientConfig } from './client/OmadaClient.js';
|
||||||
|
export { Authentication, type AuthConfig } from './auth/Authentication.js';
|
||||||
|
|
||||||
|
// Error classes
|
||||||
|
export {
|
||||||
|
OmadaApiError,
|
||||||
|
OmadaAuthenticationError,
|
||||||
|
OmadaDeviceNotFoundError,
|
||||||
|
OmadaConfigurationError,
|
||||||
|
OmadaNetworkError,
|
||||||
|
} from './errors/OmadaErrors.js';
|
||||||
|
|
||||||
|
// Type definitions
|
||||||
|
export * from './types/api.js';
|
||||||
|
export * from './types/devices.js';
|
||||||
|
export * from './types/networks.js';
|
||||||
|
export * from './types/sites.js';
|
||||||
|
|
||||||
|
// Services
|
||||||
|
export { SitesService } from './services/SitesService.js';
|
||||||
|
export { DevicesService } from './services/DevicesService.js';
|
||||||
|
export { NetworksService } from './services/NetworksService.js';
|
||||||
|
export { FirewallService } from './services/FirewallService.js';
|
||||||
|
export { SwitchService } from './services/SwitchService.js';
|
||||||
|
export { RouterService } from './services/RouterService.js';
|
||||||
|
|
||||||
111
src/services/DevicesService.ts
Normal file
111
src/services/DevicesService.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* Device management service for all Omada devices
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { OmadaClient } from '../client/OmadaClient.js';
|
||||||
|
import {
|
||||||
|
Device,
|
||||||
|
DeviceType,
|
||||||
|
DeviceStatus,
|
||||||
|
DeviceStatistics,
|
||||||
|
RouterDevice,
|
||||||
|
SwitchDevice,
|
||||||
|
} from '../types/devices.js';
|
||||||
|
|
||||||
|
export interface DeviceListOptions {
|
||||||
|
siteId?: string;
|
||||||
|
type?: DeviceType;
|
||||||
|
status?: DeviceStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DevicesService {
|
||||||
|
constructor(private client: OmadaClient) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all devices
|
||||||
|
*/
|
||||||
|
async listDevices(options: DeviceListOptions = {}): Promise<Device[]> {
|
||||||
|
const siteId = options.siteId || (await this.client.getSiteId());
|
||||||
|
const params: Record<string, string> = { siteId };
|
||||||
|
|
||||||
|
if (options.type) {
|
||||||
|
params.type = options.type;
|
||||||
|
}
|
||||||
|
if (options.status !== undefined) {
|
||||||
|
params.status = String(options.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.client.request<Device[]>('GET', '/devices', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get device by ID
|
||||||
|
*/
|
||||||
|
async getDevice(deviceId: string, siteId?: string): Promise<Device> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<Device>('GET', `/sites/${effectiveSiteId}/devices/${deviceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get device statistics
|
||||||
|
*/
|
||||||
|
async getDeviceStatistics(
|
||||||
|
deviceId: string,
|
||||||
|
siteId?: string,
|
||||||
|
startTime?: number,
|
||||||
|
endTime?: number
|
||||||
|
): Promise<DeviceStatistics[]> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (startTime) params.startTime = String(startTime);
|
||||||
|
if (endTime) params.endTime = String(endTime);
|
||||||
|
|
||||||
|
return this.client.request<DeviceStatistics[]>(
|
||||||
|
'GET',
|
||||||
|
`/sites/${effectiveSiteId}/devices/${deviceId}/statistics`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reboot a device
|
||||||
|
*/
|
||||||
|
async rebootDevice(deviceId: string, siteId?: string): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request('POST', `/sites/${effectiveSiteId}/devices/${deviceId}/reboot`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adopt a device
|
||||||
|
*/
|
||||||
|
async adoptDevice(deviceId: string, siteId?: string): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request('POST', `/sites/${effectiveSiteId}/devices/${deviceId}/adopt`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unadopt a device
|
||||||
|
*/
|
||||||
|
async unadoptDevice(deviceId: string, siteId?: string): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request('POST', `/sites/${effectiveSiteId}/devices/${deviceId}/unadopt`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get routers (ER605, etc.)
|
||||||
|
*/
|
||||||
|
async getRouters(siteId?: string): Promise<RouterDevice[]> {
|
||||||
|
const devices = await this.listDevices({ siteId, type: DeviceType.ROUTER });
|
||||||
|
return devices as RouterDevice[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get switches (SG218R, etc.)
|
||||||
|
*/
|
||||||
|
async getSwitches(siteId?: string): Promise<SwitchDevice[]> {
|
||||||
|
const devices = await this.listDevices({ siteId, type: DeviceType.SWITCH });
|
||||||
|
return devices as SwitchDevice[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
123
src/services/FirewallService.ts
Normal file
123
src/services/FirewallService.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* Firewall and NAT rules management service
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { OmadaClient } from '../client/OmadaClient.js';
|
||||||
|
import { FirewallRule, NATRule } from '../types/networks.js';
|
||||||
|
|
||||||
|
export class FirewallService {
|
||||||
|
constructor(private client: OmadaClient) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List firewall rules
|
||||||
|
*/
|
||||||
|
async listFirewallRules(siteId?: string): Promise<FirewallRule[]> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<FirewallRule[]>(
|
||||||
|
'GET',
|
||||||
|
`/sites/${effectiveSiteId}/firewall/rules`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get firewall rule by ID
|
||||||
|
*/
|
||||||
|
async getFirewallRule(ruleId: string, siteId?: string): Promise<FirewallRule> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<FirewallRule>(
|
||||||
|
'GET',
|
||||||
|
`/sites/${effectiveSiteId}/firewall/rules/${ruleId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a firewall rule
|
||||||
|
*/
|
||||||
|
async createFirewallRule(
|
||||||
|
rule: Omit<FirewallRule, 'id'>,
|
||||||
|
siteId?: string
|
||||||
|
): Promise<FirewallRule> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<FirewallRule>(
|
||||||
|
'POST',
|
||||||
|
`/sites/${effectiveSiteId}/firewall/rules`,
|
||||||
|
{ body: rule }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a firewall rule
|
||||||
|
*/
|
||||||
|
async updateFirewallRule(
|
||||||
|
ruleId: string,
|
||||||
|
rule: Partial<FirewallRule>,
|
||||||
|
siteId?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request(
|
||||||
|
'PUT',
|
||||||
|
`/sites/${effectiveSiteId}/firewall/rules/${ruleId}`,
|
||||||
|
{ body: rule }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a firewall rule
|
||||||
|
*/
|
||||||
|
async deleteFirewallRule(ruleId: string, siteId?: string): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request(
|
||||||
|
'DELETE',
|
||||||
|
`/sites/${effectiveSiteId}/firewall/rules/${ruleId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List NAT rules
|
||||||
|
*/
|
||||||
|
async listNATRules(siteId?: string): Promise<NATRule[]> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<NATRule[]>('GET', `/sites/${effectiveSiteId}/nat/rules`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get NAT rule by ID
|
||||||
|
*/
|
||||||
|
async getNATRule(ruleId: string, siteId?: string): Promise<NATRule> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<NATRule>('GET', `/sites/${effectiveSiteId}/nat/rules/${ruleId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a NAT rule
|
||||||
|
*/
|
||||||
|
async createNATRule(rule: Omit<NATRule, 'id'>, siteId?: string): Promise<NATRule> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<NATRule>('POST', `/sites/${effectiveSiteId}/nat/rules`, {
|
||||||
|
body: rule,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a NAT rule
|
||||||
|
*/
|
||||||
|
async updateNATRule(
|
||||||
|
ruleId: string,
|
||||||
|
rule: Partial<NATRule>,
|
||||||
|
siteId?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request('PUT', `/sites/${effectiveSiteId}/nat/rules/${ruleId}`, {
|
||||||
|
body: rule,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a NAT rule
|
||||||
|
*/
|
||||||
|
async deleteNATRule(ruleId: string, siteId?: string): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request('DELETE', `/sites/${effectiveSiteId}/nat/rules/${ruleId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
90
src/services/NetworksService.ts
Normal file
90
src/services/NetworksService.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* Network and VLAN configuration service
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { OmadaClient } from '../client/OmadaClient.js';
|
||||||
|
import { VLAN, NetworkProfile, DHCPConfig } from '../types/networks.js';
|
||||||
|
|
||||||
|
export class NetworksService {
|
||||||
|
constructor(private client: OmadaClient) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all VLANs
|
||||||
|
*/
|
||||||
|
async listVLANs(siteId?: string): Promise<VLAN[]> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<VLAN[]>('GET', `/sites/${effectiveSiteId}/vlans`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get VLAN by ID
|
||||||
|
*/
|
||||||
|
async getVLAN(vlanId: string, siteId?: string): Promise<VLAN> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<VLAN>('GET', `/sites/${effectiveSiteId}/vlans/${vlanId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new VLAN
|
||||||
|
*/
|
||||||
|
async createVLAN(vlan: Omit<VLAN, 'id' | 'siteId'>, siteId?: string): Promise<VLAN> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<VLAN>('POST', `/sites/${effectiveSiteId}/vlans`, {
|
||||||
|
body: vlan,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update VLAN configuration
|
||||||
|
*/
|
||||||
|
async updateVLAN(vlanId: string, vlan: Partial<VLAN>, siteId?: string): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request('PUT', `/sites/${effectiveSiteId}/vlans/${vlanId}`, {
|
||||||
|
body: vlan,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a VLAN
|
||||||
|
*/
|
||||||
|
async deleteVLAN(vlanId: string, siteId?: string): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request('DELETE', `/sites/${effectiveSiteId}/vlans/${vlanId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List network profiles
|
||||||
|
*/
|
||||||
|
async listNetworkProfiles(siteId?: string): Promise<NetworkProfile[]> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<NetworkProfile[]>('GET', `/sites/${effectiveSiteId}/network-profiles`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get network profile by ID
|
||||||
|
*/
|
||||||
|
async getNetworkProfile(profileId: string, siteId?: string): Promise<NetworkProfile> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<NetworkProfile>(
|
||||||
|
'GET',
|
||||||
|
`/sites/${effectiveSiteId}/network-profiles/${profileId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure DHCP settings
|
||||||
|
*/
|
||||||
|
async configureDHCP(
|
||||||
|
vlanId: string,
|
||||||
|
dhcpConfig: DHCPConfig,
|
||||||
|
siteId?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request(
|
||||||
|
'PUT',
|
||||||
|
`/sites/${effectiveSiteId}/vlans/${vlanId}/dhcp`,
|
||||||
|
{ body: dhcpConfig }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
144
src/services/RouterService.ts
Normal file
144
src/services/RouterService.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
/**
|
||||||
|
* Router-specific operations (ER605, etc.)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { OmadaClient } from '../client/OmadaClient.js';
|
||||||
|
import { WANPort, LANPort } from '../types/devices.js';
|
||||||
|
import { RoutingRule, DHCPConfig } from '../types/networks.js';
|
||||||
|
|
||||||
|
export class RouterService {
|
||||||
|
constructor(private client: OmadaClient) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get router WAN ports
|
||||||
|
*/
|
||||||
|
async getWANPorts(deviceId: string, siteId?: string): Promise<WANPort[]> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<WANPort[]>(
|
||||||
|
'GET',
|
||||||
|
`/sites/${effectiveSiteId}/devices/${deviceId}/wan`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure WAN port
|
||||||
|
*/
|
||||||
|
async configureWANPort(
|
||||||
|
deviceId: string,
|
||||||
|
portId: number,
|
||||||
|
config: Partial<WANPort>,
|
||||||
|
siteId?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request(
|
||||||
|
'PUT',
|
||||||
|
`/sites/${effectiveSiteId}/devices/${deviceId}/wan/${portId}`,
|
||||||
|
{ body: config }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get router LAN ports
|
||||||
|
*/
|
||||||
|
async getLANPorts(deviceId: string, siteId?: string): Promise<LANPort[]> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<LANPort[]>(
|
||||||
|
'GET',
|
||||||
|
`/sites/${effectiveSiteId}/devices/${deviceId}/lan`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure LAN port
|
||||||
|
*/
|
||||||
|
async configureLANPort(
|
||||||
|
deviceId: string,
|
||||||
|
portId: number,
|
||||||
|
config: Partial<LANPort>,
|
||||||
|
siteId?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request(
|
||||||
|
'PUT',
|
||||||
|
`/sites/${effectiveSiteId}/devices/${deviceId}/lan/${portId}`,
|
||||||
|
{ body: config }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List routing rules
|
||||||
|
*/
|
||||||
|
async listRoutingRules(deviceId: string, siteId?: string): Promise<RoutingRule[]> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<RoutingRule[]>(
|
||||||
|
'GET',
|
||||||
|
`/sites/${effectiveSiteId}/devices/${deviceId}/routing`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a routing rule
|
||||||
|
*/
|
||||||
|
async createRoutingRule(
|
||||||
|
deviceId: string,
|
||||||
|
rule: Omit<RoutingRule, 'id'>,
|
||||||
|
siteId?: string
|
||||||
|
): Promise<RoutingRule> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<RoutingRule>(
|
||||||
|
'POST',
|
||||||
|
`/sites/${effectiveSiteId}/devices/${deviceId}/routing`,
|
||||||
|
{ body: rule }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a routing rule
|
||||||
|
*/
|
||||||
|
async updateRoutingRule(
|
||||||
|
deviceId: string,
|
||||||
|
ruleId: string,
|
||||||
|
rule: Partial<RoutingRule>,
|
||||||
|
siteId?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request(
|
||||||
|
'PUT',
|
||||||
|
`/sites/${effectiveSiteId}/devices/${deviceId}/routing/${ruleId}`,
|
||||||
|
{ body: rule }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a routing rule
|
||||||
|
*/
|
||||||
|
async deleteRoutingRule(
|
||||||
|
deviceId: string,
|
||||||
|
ruleId: string,
|
||||||
|
siteId?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request(
|
||||||
|
'DELETE',
|
||||||
|
`/sites/${effectiveSiteId}/devices/${deviceId}/routing/${ruleId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure DHCP for a VLAN/network
|
||||||
|
*/
|
||||||
|
async configureDHCP(
|
||||||
|
deviceId: string,
|
||||||
|
vlanId: string,
|
||||||
|
dhcpConfig: DHCPConfig,
|
||||||
|
siteId?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request(
|
||||||
|
'PUT',
|
||||||
|
`/sites/${effectiveSiteId}/devices/${deviceId}/vlans/${vlanId}/dhcp`,
|
||||||
|
{ body: dhcpConfig }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
41
src/services/SitesService.ts
Normal file
41
src/services/SitesService.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Site management service
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { OmadaClient } from '../client/OmadaClient.js';
|
||||||
|
import { Site, SiteSettings } from '../types/sites.js';
|
||||||
|
|
||||||
|
export class SitesService {
|
||||||
|
constructor(private client: OmadaClient) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all sites
|
||||||
|
*/
|
||||||
|
async listSites(): Promise<Site[]> {
|
||||||
|
return this.client.request<Site[]>('GET', '/sites');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get site details by ID
|
||||||
|
*/
|
||||||
|
async getSite(siteId: string): Promise<Site> {
|
||||||
|
return this.client.request<Site>('GET', `/sites/${siteId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update site settings
|
||||||
|
*/
|
||||||
|
async updateSite(siteId: string, settings: Partial<SiteSettings>): Promise<void> {
|
||||||
|
await this.client.request('PUT', `/sites/${siteId}`, {
|
||||||
|
body: settings,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current site ID (or default site)
|
||||||
|
*/
|
||||||
|
async getCurrentSiteId(): Promise<string> {
|
||||||
|
return this.client.getSiteId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
96
src/services/SwitchService.ts
Normal file
96
src/services/SwitchService.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Switch-specific operations (SG218R, etc.)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { OmadaClient } from '../client/OmadaClient.js';
|
||||||
|
import { DevicePort } from '../types/devices.js';
|
||||||
|
import { SwitchPortConfig } from '../types/networks.js';
|
||||||
|
|
||||||
|
export class SwitchService {
|
||||||
|
constructor(private client: OmadaClient) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get switch ports
|
||||||
|
*/
|
||||||
|
async getSwitchPorts(deviceId: string, siteId?: string): Promise<DevicePort[]> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request<DevicePort[]>(
|
||||||
|
'GET',
|
||||||
|
`/sites/${effectiveSiteId}/devices/${deviceId}/ports`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure a switch port
|
||||||
|
*/
|
||||||
|
async configurePort(
|
||||||
|
deviceId: string,
|
||||||
|
portId: number,
|
||||||
|
config: SwitchPortConfig,
|
||||||
|
siteId?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request(
|
||||||
|
'PUT',
|
||||||
|
`/sites/${effectiveSiteId}/devices/${deviceId}/ports/${portId}`,
|
||||||
|
{ body: config }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get port statistics
|
||||||
|
*/
|
||||||
|
async getPortStatistics(
|
||||||
|
deviceId: string,
|
||||||
|
portId: number,
|
||||||
|
siteId?: string,
|
||||||
|
startTime?: number,
|
||||||
|
endTime?: number
|
||||||
|
): Promise<any> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (startTime) params.startTime = String(startTime);
|
||||||
|
if (endTime) params.endTime = String(endTime);
|
||||||
|
|
||||||
|
return this.client.request(
|
||||||
|
'GET',
|
||||||
|
`/sites/${effectiveSiteId}/devices/${deviceId}/ports/${portId}/statistics`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get PoE status and power usage
|
||||||
|
*/
|
||||||
|
async getPoEStatus(deviceId: string, siteId?: string): Promise<{
|
||||||
|
poeCapable: boolean;
|
||||||
|
totalPower: number;
|
||||||
|
usedPower: number;
|
||||||
|
ports: Array<{ portId: number; enabled: boolean; power: number }>;
|
||||||
|
}> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
return this.client.request(
|
||||||
|
'GET',
|
||||||
|
`/sites/${effectiveSiteId}/devices/${deviceId}/poe`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable/disable PoE on a port
|
||||||
|
*/
|
||||||
|
async setPoE(
|
||||||
|
deviceId: string,
|
||||||
|
portId: number,
|
||||||
|
enabled: boolean,
|
||||||
|
siteId?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const effectiveSiteId = siteId || (await this.client.getSiteId());
|
||||||
|
await this.client.request(
|
||||||
|
'PUT',
|
||||||
|
`/sites/${effectiveSiteId}/devices/${deviceId}/ports/${portId}/poe`,
|
||||||
|
{ body: { enable: enabled } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
41
src/types/api.ts
Normal file
41
src/types/api.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Core API request and response types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
errorCode: number;
|
||||||
|
msg: string;
|
||||||
|
result?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResponse<T = any> {
|
||||||
|
currentPage: number;
|
||||||
|
currentSize: number;
|
||||||
|
totalRows: number;
|
||||||
|
totalPage: number;
|
||||||
|
data: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiRequestOptions {
|
||||||
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||||
|
body?: any;
|
||||||
|
params?: Record<string, string | number | boolean>;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenResponse {
|
||||||
|
token: string;
|
||||||
|
tokenType: string;
|
||||||
|
expiresIn: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OAuth2ClientCredentials {
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
}
|
||||||
|
|
||||||
98
src/types/devices.ts
Normal file
98
src/types/devices.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Device types for Omada devices (ER605, SG218R, EAP, etc.)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum DeviceType {
|
||||||
|
ROUTER = 'router',
|
||||||
|
SWITCH = 'switch',
|
||||||
|
ACCESS_POINT = 'ap',
|
||||||
|
GATEWAY = 'gateway',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DeviceStatus {
|
||||||
|
ONLINE = 1,
|
||||||
|
OFFLINE = 0,
|
||||||
|
ADOPTING = 2,
|
||||||
|
UNADOPTED = 3,
|
||||||
|
UNKNOWN = 4,
|
||||||
|
UPGRADING = 5,
|
||||||
|
PROVISIONING = 6,
|
||||||
|
REBOOTING = 7,
|
||||||
|
DISCONNECTED = 8,
|
||||||
|
PENDING = 9,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Device {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: DeviceType;
|
||||||
|
model: string;
|
||||||
|
mac: string;
|
||||||
|
ip: string;
|
||||||
|
status: DeviceStatus;
|
||||||
|
version: string;
|
||||||
|
uptime?: number;
|
||||||
|
cpu?: number;
|
||||||
|
mem?: number;
|
||||||
|
cpuUtil?: number;
|
||||||
|
memUtil?: number;
|
||||||
|
ports?: DevicePort[];
|
||||||
|
siteId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DevicePort {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
enable: boolean;
|
||||||
|
speed?: number;
|
||||||
|
duplex?: string;
|
||||||
|
flowControl?: boolean;
|
||||||
|
status?: 'up' | 'down' | 'unknown';
|
||||||
|
vlanId?: number;
|
||||||
|
poeEnable?: boolean;
|
||||||
|
poePower?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceStatistics {
|
||||||
|
deviceId: string;
|
||||||
|
timestamp: number;
|
||||||
|
txBytes?: number;
|
||||||
|
rxBytes?: number;
|
||||||
|
txPkts?: number;
|
||||||
|
rxPkts?: number;
|
||||||
|
cpuUtil?: number;
|
||||||
|
memUtil?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RouterDevice extends Device {
|
||||||
|
type: DeviceType.ROUTER;
|
||||||
|
wanPorts?: WANPort[];
|
||||||
|
lanPorts?: LANPort[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SwitchDevice extends Device {
|
||||||
|
type: DeviceType.SWITCH;
|
||||||
|
ports: DevicePort[];
|
||||||
|
poeCapable?: boolean;
|
||||||
|
totalPoEPower?: number;
|
||||||
|
usedPoEPower?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WANPort {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
ip?: string;
|
||||||
|
gateway?: string;
|
||||||
|
dns?: string[];
|
||||||
|
status: 'up' | 'down' | 'connecting';
|
||||||
|
connectionType: 'dhcp' | 'static' | 'pppoe';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LANPort {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
ip?: string;
|
||||||
|
subnet?: string;
|
||||||
|
enable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
92
src/types/networks.ts
Normal file
92
src/types/networks.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Network and VLAN configuration types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface VLAN {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
vlanId: number;
|
||||||
|
subnet: string;
|
||||||
|
gateway: string;
|
||||||
|
dhcpEnable: boolean;
|
||||||
|
dhcpRangeStart?: string;
|
||||||
|
dhcpRangeEnd?: string;
|
||||||
|
dns1?: string;
|
||||||
|
dns2?: string;
|
||||||
|
siteId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkProfile {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: 'vlan' | 'bridge' | 'wan';
|
||||||
|
vlanId?: number;
|
||||||
|
subnet?: string;
|
||||||
|
gateway?: string;
|
||||||
|
dhcpEnable: boolean;
|
||||||
|
dhcpRangeStart?: string;
|
||||||
|
dhcpRangeEnd?: string;
|
||||||
|
dns?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FirewallRule {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
enable: boolean;
|
||||||
|
action: 'allow' | 'deny' | 'reject';
|
||||||
|
protocol: 'tcp' | 'udp' | 'tcp/udp' | 'icmp' | 'all';
|
||||||
|
srcIp?: string;
|
||||||
|
srcPort?: string;
|
||||||
|
dstIp?: string;
|
||||||
|
dstPort?: string;
|
||||||
|
direction: 'in' | 'out' | 'forward';
|
||||||
|
priority: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NATRule {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
enable: boolean;
|
||||||
|
protocol: 'tcp' | 'udp' | 'tcp/udp' | 'all';
|
||||||
|
externalIp?: string;
|
||||||
|
externalPort?: string;
|
||||||
|
internalIp: string;
|
||||||
|
internalPort: string;
|
||||||
|
interface?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DHCPConfig {
|
||||||
|
enable: boolean;
|
||||||
|
rangeStart: string;
|
||||||
|
rangeEnd: string;
|
||||||
|
leaseTime: number;
|
||||||
|
gateway: string;
|
||||||
|
dns1: string;
|
||||||
|
dns2?: string;
|
||||||
|
domain?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoutingRule {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
enable: boolean;
|
||||||
|
destination: string;
|
||||||
|
gateway: string;
|
||||||
|
interface?: string;
|
||||||
|
metric?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SwitchPortConfig {
|
||||||
|
portId: number;
|
||||||
|
enable: boolean;
|
||||||
|
name?: string;
|
||||||
|
speed?: 'auto' | '10M' | '100M' | '1G' | '2.5G' | '5G' | '10G';
|
||||||
|
duplex?: 'auto' | 'half' | 'full';
|
||||||
|
flowControl?: boolean;
|
||||||
|
vlanMode?: 'access' | 'trunk' | 'hybrid';
|
||||||
|
nativeVlanId?: number;
|
||||||
|
allowedVlans?: number[];
|
||||||
|
poeEnable?: boolean;
|
||||||
|
isolation?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
22
src/types/sites.ts
Normal file
22
src/types/sites.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Site management types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Site {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
desc?: string;
|
||||||
|
region?: string;
|
||||||
|
timezone?: string;
|
||||||
|
country?: string;
|
||||||
|
locale?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SiteSettings {
|
||||||
|
siteId: string;
|
||||||
|
name: string;
|
||||||
|
country?: string;
|
||||||
|
timezone?: string;
|
||||||
|
locale?: string;
|
||||||
|
}
|
||||||
|
|
||||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user