Initial commit: Omada API TypeScript library

This commit is contained in:
defiQUG
2025-12-21 14:17:54 -08:00
commit 9d04c8cb83
20 changed files with 1558 additions and 0 deletions

0
.env.example Normal file
View File

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
dist/
*.log
.env
.DS_Store

46
NOTES.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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';

View 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[];
}
}

View 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}`);
}
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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();
}
}

View 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
View 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
View 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
View 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
View 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
View 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"]
}