homebridge-unifi-occupancy-lite
Version:
Lightweight UniFi occupancy sensor plugin for Homebridge using API tokens only. Works with UDM Pro/SE, UDM, UDR and other UniFi OS devices.
340 lines (339 loc) • 13 kB
JavaScript
"use strict";
/*
* Based on homebridge-unifi-occupancy by DouweM
* Original: https://github.com/DouweM/homebridge-unifi-occupancy
* Licensed under Apache-2.0
*
* Modified for homebridge-unifi-occupancy-lite - supports both local controller and Site Manager API
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.UniFiLiteClient = void 0;
const https_1 = require("https");
const http_1 = require("http");
const url_1 = require("url");
class UniFiLiteClient {
constructor(config) {
this.config = config;
if (config.useSiteManagerApi) {
// Site Manager API base URL
this.baseUrl = 'https://api.ui.com';
this.controllerType = 'legacy'; // Not really used for Site Manager API
}
else {
// Detect controller type and set appropriate base URL and endpoints
this.detectControllerType();
}
}
detectControllerType() {
const controllerUrl = this.config.controller.replace(/\/$/, '');
// Check if it's likely a UniFi OS device (UDM Pro/SE, UDM, UDR)
if (controllerUrl.match(/^https?:\/\/\d+\.\d+\.\d+\.\d+$/) ||
controllerUrl.includes('.local') ||
controllerUrl.includes('unifi.ui.com')) {
// Modern UniFi OS devices
this.controllerType = 'unifi-os';
this.baseUrl = `${controllerUrl}/proxy/network`;
}
else if (controllerUrl.includes(':8443')) {
// Cloud Key or controller on port 8443
this.controllerType = 'cloud-key';
this.baseUrl = controllerUrl;
}
else {
// Legacy controller or other
this.controllerType = 'legacy';
this.baseUrl = controllerUrl;
}
}
/**
* Make HTTP request to UniFi API using Node.js built-in modules
*/
async request(endpoint, options = {}) {
// Properly construct full URL by concatenating baseUrl and endpoint
const fullUrl = `${this.baseUrl}${endpoint}`;
const url = new url_1.URL(fullUrl);
const headers = {
'Content-Type': 'application/json',
};
// Set appropriate auth header based on API type
if (this.config.useSiteManagerApi) {
headers['X-API-Key'] = this.config.apiKey;
}
else {
headers['X-API-KEY'] = this.config.apiKey;
}
const requestOptions = {
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname + url.search,
method: options.method || 'GET',
headers,
rejectUnauthorized: this.config.secure
};
return new Promise((resolve, reject) => {
const req = (url.protocol === 'https:' ? https_1.request : http_1.request)(requestOptions, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
if (res.statusCode && res.statusCode >= 400) {
// Enhanced error reporting with response body
let errorMessage = `UniFi API Error: HTTP ${res.statusCode} - ${res.statusMessage}`;
// Try to parse error details from response
try {
const errorData = JSON.parse(data);
if (errorData.message) {
errorMessage += ` | Message: ${errorData.message}`;
}
if (errorData.error) {
errorMessage += ` | Error: ${errorData.error}`;
}
if (errorData.details) {
errorMessage += ` | Details: ${JSON.stringify(errorData.details)}`;
}
}
catch (parseError) {
// If JSON parsing fails, include raw response
if (data && data.length > 0 && data.length < 500) {
errorMessage += ` | Response: ${data}`;
}
}
// Add request details for debugging
errorMessage += ` | Request: ${options.method || 'GET'} ${fullUrl}`;
reject(new Error(errorMessage));
return;
}
const jsonData = JSON.parse(data);
if (this.config.useSiteManagerApi) {
// Site Manager API returns data directly in data field
resolve(jsonData.data !== undefined ? jsonData.data : jsonData);
}
else {
// Local controller API wraps data in { meta: {}, data: [] }
resolve(jsonData.data !== undefined ? jsonData.data : jsonData);
}
}
catch (error) {
reject(new Error(`Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)} | Raw response: ${data.substring(0, 200)}`));
}
});
});
req.on('error', (error) => {
reject(new Error(`UniFi API request failed: ${error.message} | Request: ${options.method || 'GET'} ${fullUrl}`));
});
if (options.body) {
req.write(options.body);
}
req.end();
});
}
/**
* GET request
*/
async get(endpoint) {
return this.request(endpoint, { method: 'GET' });
}
/**
* POST request
*/
async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
});
}
/**
* Get all sites
*/
async getSites() {
if (this.config.useSiteManagerApi) {
return this.get('/v1/sites');
}
else {
return this.get('/api/stat/sites');
}
}
/**
* Get active clients for a site
*/
async getClients(site = this.config.site) {
if (this.config.useSiteManagerApi) {
// For Site Manager API, we need to get devices and filter clients
return this.getDevices().then(devices => {
return devices.flatMap(host => host.devices?.filter(device => device.productLine === 'network') || []);
});
}
else {
// Use correct endpoint based on controller type
if (this.controllerType === 'unifi-os') {
// UniFi OS uses stat/sta for station statistics with access point info
return this.get(`/api/s/${site}/stat/sta`);
}
else {
// Cloud Key and Legacy use clients/active
return this.get(`/api/s/${site}/clients/active`);
}
}
}
/**
* Get devices using Site Manager API or local controller
*/
async getDevices() {
if (this.config.useSiteManagerApi) {
const params = this.config.hostId ? `?hostIds[]=${this.config.hostId}` : '';
return this.get(`/v1/devices${params}`);
}
else {
return this.getNetworkDevices();
}
}
/**
* Get network devices (access points, switches)
*/
async getNetworkDevices(site = this.config.site) {
if (this.config.useSiteManagerApi) {
return this.getDevices().then(devices => {
return devices.flatMap(host => host.devices?.filter(device => device.productLine === 'network' &&
(device.model?.includes('AP') || device.model?.includes('Switch'))) || []);
});
}
else {
// All controller types use the same device endpoint
return this.get(`/api/s/${site}/stat/device`);
}
}
/**
* Get device fingerprints database
*/
async getDeviceFingerprints() {
if (this.config.useSiteManagerApi) {
// Site Manager API doesn't have fingerprint endpoint, return empty object
return {};
}
else {
return this.get('/api/fingerprint_devices/0');
}
}
/**
* Get client statistics including traffic data
*/
async getClientStats(mac, site = this.config.site) {
if (this.config.useSiteManagerApi) {
// Site Manager API doesn't have detailed client stats, return null
return null;
}
else {
return this.get(`/api/site/${site}/stat/client/${mac}`);
}
}
/**
* Get site statistics
*/
async getSiteStats(site = this.config.site) {
if (this.config.useSiteManagerApi) {
// Site Manager API doesn't have site stats, return null
return null;
}
else {
return this.get(`/api/site/${site}/stat/sites`);
}
}
/**
* Get ISP metrics (Site Manager API only)
*/
async getIspMetrics() {
if (this.config.useSiteManagerApi) {
const params = this.config.hostId ? `?hostIds[]=${this.config.hostId}` : '';
return this.get(`/v1/isp-metrics${params}`);
}
else {
return null;
}
}
/**
* Get client traffic data for the last 15 minutes
*/
async getClientTrafficLast15Min(mac) {
try {
if (this.config.useSiteManagerApi) {
// Site Manager API doesn't provide detailed traffic stats
// This would need to be implemented if the API supports it in the future
return null;
}
else {
// For local controller, try to get recent client stats
const stats = await this.getClientStats(mac);
if (stats && stats.length > 0) {
const client = stats[0];
// Return recent traffic data if available
return {
rx_bytes: client.rx_bytes || 0,
tx_bytes: client.tx_bytes || 0
};
}
}
}
catch (error) {
// Traffic data is optional, don't throw error
return null;
}
return null;
}
/**
* For compatibility with original code
*/
get opts() {
return { site: this.config.site };
}
/**
* Test API connection
*/
async testConnection() {
try {
if (this.config.useSiteManagerApi) {
await this.getDevices();
return true;
}
else {
// Try multiple endpoints to test connection
try {
// First try sites endpoint
await this.getSites();
return true;
}
catch (sitesError) {
const errorMsg = sitesError instanceof Error ? sitesError.message : String(sitesError);
console.log(`Sites endpoint failed: ${errorMsg}`);
try {
// Try devices endpoint as fallback
await this.getNetworkDevices();
return true;
}
catch (devicesError) {
const devErrorMsg = devicesError instanceof Error ? devicesError.message : String(devicesError);
console.log(`Devices endpoint failed: ${devErrorMsg}`);
try {
// Try clients endpoint as last resort
await this.getClients();
return true;
}
catch (clientsError) {
const clientErrorMsg = clientsError instanceof Error ? clientsError.message : String(clientsError);
console.log(`Clients endpoint failed: ${clientErrorMsg}`);
console.log(`Base URL used: ${this.baseUrl}`);
return false;
}
}
}
}
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.log(`Connection test failed: ${errorMsg}`);
return false;
}
}
}
exports.UniFiLiteClient = UniFiLiteClient;