feat(unifi): implement comprehensive UniFi API client with controllers, protect, access, account, managers, resources, HTTP client, interfaces, logging, plugins, and tests
This commit is contained in:
275
ts/classes.camera.ts
Normal file
275
ts/classes.camera.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import type { UnifiProtect } from './classes.unifi-protect.js';
|
||||
import type {
|
||||
IProtectCamera,
|
||||
IProtectCameraChannel,
|
||||
IProtectRecordingSettings,
|
||||
IProtectSmartDetectSettings,
|
||||
IProtectIspSettings,
|
||||
IProtectFeatureFlags,
|
||||
IProtectCameraStats,
|
||||
} from './interfaces/index.js';
|
||||
|
||||
/**
|
||||
* Represents a UniFi Protect camera
|
||||
*/
|
||||
export class UnifiCamera implements IProtectCamera {
|
||||
/** Reference to parent protect instance */
|
||||
private protect?: UnifiProtect;
|
||||
|
||||
// IProtectCamera properties
|
||||
public id: string;
|
||||
public mac: string;
|
||||
public host: string;
|
||||
public name: string;
|
||||
public type: string;
|
||||
public modelKey?: string;
|
||||
public state: 'CONNECTED' | 'DISCONNECTED' | 'CONNECTING' | 'ADOPTING' | 'MANAGED';
|
||||
public hardwareRevision?: string;
|
||||
public firmwareVersion?: string;
|
||||
public firmwareBuild?: string;
|
||||
public isUpdating?: boolean;
|
||||
public isAdopting?: boolean;
|
||||
public isManaged?: boolean;
|
||||
public isConnected?: boolean;
|
||||
public isRecording?: boolean;
|
||||
public isMotionDetected?: boolean;
|
||||
public isDark?: boolean;
|
||||
public recordingSettings?: IProtectRecordingSettings;
|
||||
public smartDetectSettings?: IProtectSmartDetectSettings;
|
||||
public ispSettings?: IProtectIspSettings;
|
||||
public micVolume?: number;
|
||||
public speakerVolume?: number;
|
||||
public lastMotion?: number;
|
||||
public lastRing?: number;
|
||||
public uptime?: number;
|
||||
public connectedSince?: number;
|
||||
public upSince?: number;
|
||||
public lastSeen?: number;
|
||||
public channels?: IProtectCameraChannel[];
|
||||
public featureFlags?: IProtectFeatureFlags;
|
||||
public stats?: IProtectCameraStats;
|
||||
|
||||
constructor() {
|
||||
this.id = '';
|
||||
this.mac = '';
|
||||
this.host = '';
|
||||
this.name = '';
|
||||
this.type = '';
|
||||
this.state = 'DISCONNECTED';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a camera instance from API response object
|
||||
*/
|
||||
public static createFromApiObject(
|
||||
apiObject: IProtectCamera,
|
||||
protect?: UnifiProtect
|
||||
): UnifiCamera {
|
||||
const camera = new UnifiCamera();
|
||||
Object.assign(camera, apiObject);
|
||||
camera.protect = protect;
|
||||
return camera;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw API object representation
|
||||
*/
|
||||
public toApiObject(): IProtectCamera {
|
||||
return {
|
||||
id: this.id,
|
||||
mac: this.mac,
|
||||
host: this.host,
|
||||
name: this.name,
|
||||
type: this.type,
|
||||
modelKey: this.modelKey,
|
||||
state: this.state,
|
||||
hardwareRevision: this.hardwareRevision,
|
||||
firmwareVersion: this.firmwareVersion,
|
||||
firmwareBuild: this.firmwareBuild,
|
||||
isUpdating: this.isUpdating,
|
||||
isAdopting: this.isAdopting,
|
||||
isManaged: this.isManaged,
|
||||
isConnected: this.isConnected,
|
||||
isRecording: this.isRecording,
|
||||
isMotionDetected: this.isMotionDetected,
|
||||
isDark: this.isDark,
|
||||
recordingSettings: this.recordingSettings,
|
||||
smartDetectSettings: this.smartDetectSettings,
|
||||
ispSettings: this.ispSettings,
|
||||
micVolume: this.micVolume,
|
||||
speakerVolume: this.speakerVolume,
|
||||
lastMotion: this.lastMotion,
|
||||
lastRing: this.lastRing,
|
||||
uptime: this.uptime,
|
||||
connectedSince: this.connectedSince,
|
||||
upSince: this.upSince,
|
||||
lastSeen: this.lastSeen,
|
||||
channels: this.channels,
|
||||
featureFlags: this.featureFlags,
|
||||
stats: this.stats,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if camera is online
|
||||
*/
|
||||
public isOnline(): boolean {
|
||||
return this.state === 'CONNECTED' && this.isConnected === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if camera is a doorbell
|
||||
*/
|
||||
public isDoorbell(): boolean {
|
||||
return this.type.toLowerCase().includes('doorbell');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if camera has smart detection capability
|
||||
*/
|
||||
public hasSmartDetect(): boolean {
|
||||
return this.featureFlags?.hasSmartDetect === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get RTSP stream URL for a channel
|
||||
*/
|
||||
public getRtspUrl(channelId: number = 0): string | null {
|
||||
const channel = this.channels?.find((c) => c.id === channelId);
|
||||
if (!channel || !channel.isRtspEnabled || !channel.rtspAlias) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// RTSP URL format: rtsp://{host}:7447/{rtspAlias}
|
||||
return `rtsp://${this.host}:7447/${channel.rtspAlias}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the high quality channel
|
||||
*/
|
||||
public getHighQualityChannel(): IProtectCameraChannel | null {
|
||||
// Channel 0 is typically the highest quality
|
||||
return this.channels?.find((c) => c.id === 0) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the medium quality channel
|
||||
*/
|
||||
public getMediumQualityChannel(): IProtectCameraChannel | null {
|
||||
// Channel 1 is typically medium quality
|
||||
return this.channels?.find((c) => c.id === 1) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the low quality channel
|
||||
*/
|
||||
public getLowQualityChannel(): IProtectCameraChannel | null {
|
||||
// Channel 2 is typically low quality
|
||||
return this.channels?.find((c) => c.id === 2) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update camera settings
|
||||
*/
|
||||
public async updateSettings(settings: Partial<IProtectCamera>): Promise<void> {
|
||||
if (!this.protect) {
|
||||
throw new Error('Cannot update camera: no protect reference');
|
||||
}
|
||||
|
||||
await this.protect.request('PATCH', `/cameras/${this.id}`, settings);
|
||||
|
||||
// Update local state
|
||||
Object.assign(this, settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set recording mode
|
||||
*/
|
||||
public async setRecordingMode(
|
||||
mode: 'always' | 'detections' | 'never' | 'schedule'
|
||||
): Promise<void> {
|
||||
await this.updateSettings({
|
||||
recordingSettings: {
|
||||
...this.recordingSettings,
|
||||
mode,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable smart detection types
|
||||
*/
|
||||
public async setSmartDetectTypes(types: string[]): Promise<void> {
|
||||
await this.updateSettings({
|
||||
smartDetectSettings: {
|
||||
...this.smartDetectSettings,
|
||||
objectTypes: types,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set microphone volume
|
||||
*/
|
||||
public async setMicVolume(volume: number): Promise<void> {
|
||||
const clampedVolume = Math.max(0, Math.min(100, volume));
|
||||
await this.updateSettings({ micVolume: clampedVolume });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set speaker volume
|
||||
*/
|
||||
public async setSpeakerVolume(volume: number): Promise<void> {
|
||||
const clampedVolume = Math.max(0, Math.min(100, volume));
|
||||
await this.updateSettings({ speakerVolume: clampedVolume });
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the camera
|
||||
*/
|
||||
public async rename(newName: string): Promise<void> {
|
||||
await this.updateSettings({ name: newName });
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the camera
|
||||
*/
|
||||
public async restart(): Promise<void> {
|
||||
if (!this.protect) {
|
||||
throw new Error('Cannot restart camera: no protect reference');
|
||||
}
|
||||
|
||||
await this.protect.request('POST', `/cameras/${this.id}/reboot`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get snapshot URL
|
||||
*/
|
||||
public getSnapshotUrl(): string {
|
||||
if (!this.protect) {
|
||||
throw new Error('Cannot get snapshot URL: no protect reference');
|
||||
}
|
||||
|
||||
return `/proxy/protect/api/cameras/${this.id}/snapshot`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if camera has motion in the last N seconds
|
||||
*/
|
||||
public hasRecentMotion(seconds: number = 60): boolean {
|
||||
if (!this.lastMotion) return false;
|
||||
|
||||
const now = Date.now();
|
||||
const motionAge = (now - this.lastMotion) / 1000;
|
||||
return motionAge <= seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time since last motion in seconds
|
||||
*/
|
||||
public getTimeSinceLastMotion(): number | null {
|
||||
if (!this.lastMotion) return null;
|
||||
|
||||
return Math.floor((Date.now() - this.lastMotion) / 1000);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user