Files
unifi/ts/classes.camera.ts

276 lines
7.1 KiB
TypeScript
Raw Normal View History

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