UNPKG

homebridge-eufy-security

Version:
431 lines 19.2 kB
import { DeviceAccessory } from './Device.js'; // @ts-ignore import { PropertyName, CommandName } from 'eufy-security-client'; import { DEFAULT_CAMERACONFIG_VALUES } from '../utils/configTypes.js'; import { CHAR, SERV } from '../utils/utils.js'; import { StreamingDelegate } from '../controller/streamingDelegate.js'; import { RecordingDelegate } from '../controller/recordingDelegate.js'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ export class CameraAccessory extends DeviceAccessory { // Define the object variable to hold the boolean and timestamp cameraStatus; notificationTimeout = null; cameraConfig; hardwareTranscoding = true; hardwareDecoding = true; timeshift = false; hksvRecording = true; HksvErrors = 0; isOnline = true; rtsp_url = ''; metadata; standalone = false; // List of event types eventTypesToHandle = [ 'motion detected', 'person detected', 'pet detected', 'vehicle detected', 'sound detected', 'crying detected', 'dog detected', 'stranger person detected', ]; streamingDelegate = null; recordingDelegate = null; resolutions = [ [320, 180, 30], [320, 240, 15], // Apple Watch requires this configuration [320, 240, 30], [480, 270, 30], [480, 360, 30], [640, 360, 30], [640, 480, 30], [1280, 720, 30], [1280, 960, 30], [1920, 1080, 30], [1600, 1200, 30], ]; constructor(platform, accessory, device) { super(platform, accessory, device); this.cameraConfig = {}; this.cameraStatus = { isEnabled: false, timestamp: 0 }; // Initialize the cameraStatus object this.log.debug(`Constructed Camera`); this.cameraConfig = this.getCameraConfig(); this.standalone = device.getSerial() === device.getStationSerial(); this.log.debug(`Is standalone?`, this.standalone); if (this.cameraConfig.enableCamera) { this.log.debug(`has a camera: Setting up camera.`); this.setupCamera(); } else { this.log.debug(`has a motion sensor: Setting up motion.`); this.setupMotionFunction(); } this.initSensorService(); this.setupEnableButton(); this.setupMotionButton(); this.setupLightButton(); this.setupChimeButton(); this.pruneUnusedServices(); } setupCamera() { try { this.cameraFunction(); } catch (error) { this.log.error(`while happending CameraFunction ${error}`); } try { this.configureVideoStream(); } catch (error) { this.log.error(`while happending Delegate ${error}`); } } setupButtonService(serviceName, configValue, PropertyName, serviceType) { try { this.log.debug(`${serviceName} config:`, configValue); if (configValue && this.device.hasProperty(PropertyName)) { this.log.debug(`has a ${PropertyName}, so append ${serviceType}${serviceName} characteristic to it.`); this.setupSwitchService(serviceName, serviceType, PropertyName); } else { this.log.debug(`Looks like not compatible with ${PropertyName} or this has been disabled within configuration`); } } catch (error) { this.log.error(`raise error to check and attach ${serviceType}${serviceName}.`, error); throw error; } } setupSwitchService(serviceName, serviceType, propertyName) { const platformServiceMapping = { switch: SERV.Switch, lightbulb: SERV.Lightbulb, outlet: SERV.Outlet, }; this.registerCharacteristic({ serviceType: platformServiceMapping[serviceType] || SERV.Switch, characteristicType: CHAR.On, name: this.accessory.displayName + ' ' + serviceName, serviceSubType: serviceName, getValue: (data, characteristic) => this.getCameraPropertyValue(characteristic, propertyName), setValue: (value, characteristic) => this.setCameraPropertyValue(characteristic, propertyName, value), }); } async setupEnableButton() { this.setupButtonService('Enabled', this.cameraConfig.enableButton, PropertyName.DeviceEnabled, 'switch'); } async setupMotionButton() { this.setupButtonService('Motion', this.cameraConfig.motionButton, PropertyName.DeviceMotionDetection, 'switch'); } async setupLightButton() { this.setupButtonService('Light', this.cameraConfig.lightButton, PropertyName.DeviceLight, 'lightbulb'); } async setupChimeButton() { this.setupButtonService('IndoorChime', this.cameraConfig.indoorChimeButton, PropertyName.DeviceChimeIndoor, 'switch'); } /** * Get the configuration for a camera device. * * - Combines default settings with those from the platform config. * - Validates certain settings like talkback capability. * * @returns {CameraConfig} The finalized camera configuration. */ getCameraConfig() { // Find the specific camera config from the platform based on its serial number const foundConfig = this.platform.config.cameras?.find(e => e.serialNumber === this.device.getSerial()) ?? {}; // Combine default and specific configurations const config = { ...DEFAULT_CAMERACONFIG_VALUES, ...foundConfig, name: this.accessory.displayName, }; // Initialize videoConfig if it's undefined if (!config.videoConfig) { config.videoConfig = {}; } config.videoConfig.debug = config.videoConfig?.debug ?? true; // Validate talkback setting if (config.talkback && !this.device.hasCommand(CommandName.DeviceStartTalkback)) { this.log.warn('Talkback for this device is not supported!'); config.talkback = false; } // Validate talkback with rtsp setting if (config.talkback && config.rtsp) { this.log.warn('Talkback cannot be used with rtsp option. Ignoring talkback setting.'); config.talkback = false; } this.log.debug(`config is`, config); return config; } cameraFunction() { // Fire snapshot when motion detected this.registerCharacteristic({ serviceType: SERV.MotionSensor, characteristicType: CHAR.MotionDetected, getValue: () => this.device.getPropertyValue(PropertyName.DeviceMotionDetected), onValue: (service, characteristic) => { this.eventTypesToHandle.forEach(eventType => { this.device.on(eventType, (device, state) => { this.log.info(`MOTION DETECTED (${eventType})': ${state}`); characteristic.updateValue(state); }); }); }, }); // if (this.device.hasProperty('speaker')) { // this.registerCharacteristic({ // serviceType: SERV.Speaker, // characteristicType: CHAR.Mute, // serviceSubType: 'speaker_mute', // getValue: (data, characteristic) => // this.getCameraPropertyValue(characteristic, PropertyName.DeviceSpeaker), // setValue: (value, characteristic) => // this.setCameraPropertyValue(characteristic, PropertyName.DeviceSpeaker, value), // }); // } // if (this.device.hasProperty('speakerVolume')) { // this.registerCharacteristic({ // serviceType: SERV.Speaker, // characteristicType: CHAR.Volume, // serviceSubType: 'speaker_volume', // getValue: (data, characteristic) => // this.getCameraPropertyValue(characteristic, PropertyName.DeviceSpeakerVolume), // setValue: (value, characteristic) => // this.setCameraPropertyValue(characteristic, PropertyName.DeviceSpeakerVolume, value), // }); // } // if (this.device.hasProperty('microphone')) { // this.registerCharacteristic({ // serviceType: SERV.Microphone, // characteristicType: CHAR.Mute, // serviceSubType: 'mic_mute', // getValue: (data, characteristic) => // this.getCameraPropertyValue(characteristic, PropertyName.DeviceMicrophone), // setValue: (value, characteristic) => // this.setCameraPropertyValue(characteristic, PropertyName.DeviceMicrophone, value), // }); // } if (this.device.isDoorbell()) { this.registerCharacteristic({ serviceType: SERV.Doorbell, characteristicType: CHAR.ProgrammableSwitchEvent, onValue: (service, characteristic) => { this.device.on('rings', () => this.onDeviceRingsPushNotification(characteristic)); }, }); } } // This private function sets up the motion sensor characteristics for the accessory. setupMotionFunction() { // Register the motion sensor characteristic for detecting motion. this.registerCharacteristic({ serviceType: SERV.MotionSensor, characteristicType: CHAR.MotionDetected, getValue: () => this.device.getPropertyValue(PropertyName.DeviceMotionDetected), onMultipleValue: this.eventTypesToHandle, }); // If the camera is disabled, flag the motion sensor as tampered. // This is done because the motion sensor won't work until the camera is enabled again. this.registerCharacteristic({ serviceType: SERV.MotionSensor, characteristicType: CHAR.StatusTampered, getValue: () => { const tampered = this.device.getPropertyValue(PropertyName.DeviceEnabled); this.log.debug(`TAMPERED? ${!tampered}`); return tampered ? CHAR.StatusTampered.NOT_TAMPERED : CHAR.StatusTampered.TAMPERED; }, }); if (this.device.isDoorbell()) { this.registerCharacteristic({ serviceType: SERV.Doorbell, characteristicType: CHAR.ProgrammableSwitchEvent, onValue: (service, characteristic) => { this.device.on('rings', () => this.onDeviceRingsPushNotification(characteristic)); }, }); } } getCameraPropertyValue(characteristic, propertyName) { try { const value = this.device.getPropertyValue(propertyName); return this.applyPropertyValue(characteristic, propertyName, value); } catch (error) { this.log.debug(`Error getting '${characteristic.displayName}' ${propertyName}: ${error}`); return false; } } applyPropertyValue(characteristic, propertyName, value) { this.log.debug(`GET '${characteristic.displayName}' ${propertyName}: ${value}`); if (propertyName === PropertyName.DeviceNightvision) { return value === 1; } // Override for PropertyName.DeviceEnabled when enabled button is fired and if (propertyName === PropertyName.DeviceEnabled && Date.now() - this.cameraStatus.timestamp <= 60000) { this.log.debug(`CACHED for (1 min) '${characteristic.displayName}' ${propertyName}: ${this.cameraStatus.isEnabled}`); value = this.cameraStatus.isEnabled; } if (characteristic.displayName === 'Manually Disabled') { value = !value; this.log.debug(`INVERSED '${characteristic.displayName}' ${propertyName}: ${value}`); } if (value === undefined) { throw new Error(`Value is undefined: this shouldn't happend`); } return value; } async setCameraPropertyValue(characteristic, propertyName, value) { try { this.log.debug(`SET '${characteristic.displayName}' ${propertyName}: ${value}`); await this.setPropertyValue(propertyName, value); if (propertyName === PropertyName.DeviceEnabled && characteristic.displayName === 'On') { characteristic.updateValue(value); this.cameraStatus = { isEnabled: value, timestamp: Date.now() }; characteristic = this.getService(SERV.CameraOperatingMode) .getCharacteristic(CHAR.ManuallyDisabled); this.log.debug(`INVERSED '${characteristic.displayName}' ${propertyName}: ${!value}`); value = !value; } characteristic.updateValue(value); } catch (error) { this.log.debug(`Error setting '${characteristic.displayName}' ${propertyName}: ${error}`); } } /** * Handle push notifications for a doorbell device. * Mute subsequent notifications within a timeout period. * @param characteristic - The Characteristic to update for HomeKit. */ onDeviceRingsPushNotification(characteristic) { if (!this.notificationTimeout) { this.log.debug(`DoorBell ringing`); characteristic.updateValue(CHAR.ProgrammableSwitchEvent.SINGLE_PRESS); // Set a new timeout for muting subsequent notifications this.notificationTimeout = setTimeout(() => { this.notificationTimeout = null; }, 15 * 1000); } } // Get the current bitrate for a specific camera channel. getBitrate() { return -1; } // Set the bitrate for a specific camera channel. async setBitrate() { return true; } // Configure a camera accessory for HomeKit. configureVideoStream() { this.log.debug(`configureVideoStream`); try { this.log.debug(`StreamingDelegate`); this.streamingDelegate = new StreamingDelegate(this); this.log.debug(`RecordingDelegate`); this.recordingDelegate = new RecordingDelegate(this.platform, this.accessory, this.device, this.cameraConfig, this.streamingDelegate.getLivestreamManager(), this.streamingDelegate.getSnapshotDelegate()); this.log.debug(`Controller`); const controller = new this.platform.api.hap.CameraController(this.getCameraControllerOptions()); this.log.debug(`streamingDelegate.setController`); this.streamingDelegate.setController(controller); this.log.debug(`recordingDelegate.setController`); this.recordingDelegate.setController(controller); this.log.debug(`configureController`); // Remove stale controller-managed services from cache before configuring. // When HSV is enabled, CameraController creates CameraOperatingMode and // DataStreamTransportManagement services automatically. If the cached // accessory already has them (e.g. from a previous run), configureController // will throw a duplicate UUID error. const controllerManagedServiceUUIDs = [ SERV.CameraOperatingMode.UUID, SERV.DataStreamTransportManagement.UUID, ]; for (const uuid of controllerManagedServiceUUIDs) { const existingService = this.accessory.services.find(s => s.UUID === uuid); if (existingService) { this.log.debug(`Removing stale cached service ${uuid} before configureController`); this.accessory.removeService(existingService); } } this.accessory.configureController(controller); } catch (error) { this.log.error(`while happending Delegate ${error}`); } return true; } getCameraControllerOptions() { const option = { cameraStreamCount: this.cameraConfig.videoConfig?.maxStreams || 2, // HomeKit requires at least 2 streams, but 1 is also just fine delegate: this.streamingDelegate, streamingOptions: { supportedCryptoSuites: [0 /* SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80 */], video: { resolutions: this.resolutions, codec: { profiles: [0 /* H264Profile.BASELINE */, 1 /* H264Profile.MAIN */, 2 /* H264Profile.HIGH */], levels: [0 /* H264Level.LEVEL3_1 */, 1 /* H264Level.LEVEL3_2 */, 2 /* H264Level.LEVEL4_0 */], }, }, audio: { twoWayAudio: this.cameraConfig.talkback, codecs: [ { type: "AAC-eld" /* AudioStreamingCodecType.AAC_ELD */, samplerate: 16 /* AudioStreamingSamplerate.KHZ_16 */, }, ], }, }, recording: { options: { overrideEventTriggerOptions: [ 1 /* EventTriggerOption.MOTION */, 2 /* EventTriggerOption.DOORBELL */, ], prebufferLength: 0, // prebufferLength always remains 4s ? mediaContainerConfiguration: [ { type: 0 /* MediaContainerType.FRAGMENTED_MP4 */, fragmentLength: 4000, }, ], video: { type: 0 /* this.platform.api.hap.VideoCodecType.H264 */, parameters: { profiles: [0 /* H264Profile.BASELINE */, 1 /* H264Profile.MAIN */, 2 /* H264Profile.HIGH */], levels: [0 /* H264Level.LEVEL3_1 */, 1 /* H264Level.LEVEL3_2 */, 2 /* H264Level.LEVEL4_0 */], }, resolutions: this.resolutions, }, audio: { codecs: { type: 1 /* AudioRecordingCodecType.AAC_ELD */, samplerate: 2 /* AudioRecordingSamplerate.KHZ_24 */, bitrateMode: 0, audioChannels: 1, }, }, }, delegate: this.recordingDelegate, }, sensors: { motion: this.getService(SERV.MotionSensor), occupancy: undefined, }, }; return option; } } //# sourceMappingURL=CameraAccessory.js.map