276 lines
7.1 KiB
TypeScript
276 lines
7.1 KiB
TypeScript
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);
|
|
}
|
|
}
|