UNPKG

homebridge-eufy-security-mikebcbc

Version:
484 lines 24.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CameraAccessory = void 0; const Device_1 = require("./Device"); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const eufy_security_client_1 = require("eufy-security-client"); const streamingDelegate_1 = require("../controller/streamingDelegate"); const recordingDelegate_1 = require("../controller/recordingDelegate"); const configTypes_1 = require("../utils/configTypes"); const settings_1 = require("../settings"); /** * 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. */ class CameraAccessory extends Device_1.DeviceAccessory { constructor(platform, accessory, device) { super(platform, accessory, device); this.notificationTimeout = null; this.hardwareTranscoding = true; this.hardwareDecoding = true; this.timeshift = false; this.hksvRecording = true; this.HksvErrors = 0; this.isOnline = true; this.rtspEntries = []; this.rtspQuality = {}; this.isVideoConfigured = false; this.rtsp_url = ''; // List of event types this.eventTypesToHandle = [ 'motion detected', 'person detected', 'pet detected', 'vehicle detected', 'sound detected', 'crying detected', 'dog detected', 'stranger person detected', ]; this.streamingDelegate = null; this.recordingDelegate = null; this.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], ]; this.cameraConfig = {}; this.cameraStatus = { isEnabled: false, timestamp: 0 }; // Initialize the cameraStatus object this.log.debug(`${this.accessory.displayName} Constructed Camera`); this.cameraConfig = this.getCameraConfig(); if (this.cameraConfig.enableCamera || this.device.isDoorbell()) { this.log.debug(`${this.accessory.displayName} has a camera`); this.setupCamera(); this.setupChimeButton(); this.initSensorService(this.platform.Service.Battery); } else { this.log.debug(`${this.accessory.displayName} has a motion sensor`); this.setupMotionFunction(); this.initSensorService(this.platform.Service.MotionSensor); } this.setupEnableButton(); this.setupMotionButton(); this.setupLightButton(); this.pruneUnusedServices(); } setupCamera() { try { this.cameraFunction(); } catch (error) { this.log.error(`${this.accessory.displayName} while happending CameraFunction ${error}`); } try { this.configureVideoStream(); } catch (error) { this.log.error(`${this.accessory.displayName} while happending Delegate ${error}`); } } getCameraStreamingOptions() { return { 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 */, bitrate: 0 /* AudioBitrate.VARIABLE */, audioChannels: 1, }, ], }, }; } /** * Get camera recording options based on device capabilities. * @returns CameraRecordingOptions object */ getCameraRecordingOptions() { // Determine event trigger options based on device type const eventTriggerOptions = [1 /* EventTriggerOption.MOTION */]; if (this.device.isDoorbell()) { eventTriggerOptions.push(2 /* EventTriggerOption.DOORBELL */); } return { overrideEventTriggerOptions: eventTriggerOptions, prebufferLength: settings_1.PROTECT_HKSV_TIMESHIFT_BUFFER_MAXLENGTH, mediaContainerConfiguration: [ { type: 0 /* MediaContainerType.FRAGMENTED_MP4 */, fragmentLength: settings_1.PROTECT_HKSV_SEGMENT_LENGTH, }, ], video: { type: 0 /* this.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: 0 /* AudioRecordingCodecType.AAC_LC */, samplerate: 1 /* AudioRecordingSamplerate.KHZ_16 */, }, }, }; } getCameraControllerOptions() { var _a; const option = { cameraStreamCount: ((_a = this.cameraConfig.videoConfig) === null || _a === void 0 ? void 0 : _a.maxStreams) || 2, // HomeKit requires at least 2 streams, but 1 is also just fine delegate: this.streamingDelegate, streamingOptions: this.getCameraStreamingOptions(), recording: this.cameraConfig.hsv ? { options: this.getCameraRecordingOptions(), delegate: this.recordingDelegate, } : undefined, sensors: this.cameraConfig.hsv ? { motion: this.getService(this.platform.Service.MotionSensor), occupancy: undefined, } : undefined, }; return option; } setupButtonService(serviceName, configValue, PropertyName, serviceType) { try { this.log.debug(`${this.accessory.displayName} ${serviceName} config:`, configValue); if (configValue && this.device.hasProperty(PropertyName)) { // eslint-disable-next-line max-len this.log.debug(`${this.accessory.displayName} has a ${PropertyName}, so append ${serviceType}${serviceName} characteristic to it.`); this.setupSwitchService(serviceName, serviceType, PropertyName); } else { // eslint-disable-next-line max-len this.log.debug(`${this.accessory.displayName} Looks like not compatible with ${PropertyName} or this has been disabled within configuration`); } } catch (error) { this.log.error(`${this.accessory.displayName} raise error to check and attach ${serviceType}${serviceName}.`, error); throw Error; } } setupSwitchService(serviceName, serviceType, propertyName) { const platformServiceMapping = { switch: this.platform.Service.Switch, lightbulb: this.platform.Service.Lightbulb, outlet: this.platform.Service.Outlet, }; this.registerCharacteristic({ serviceType: platformServiceMapping[serviceType] || this.platform.Service.Switch, characteristicType: this.platform.Characteristic.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, eufy_security_client_1.PropertyName.DeviceEnabled, 'switch'); } async setupMotionButton() { this.setupButtonService('Motion', this.cameraConfig.motionButton, eufy_security_client_1.PropertyName.DeviceMotionDetection, 'switch'); } async setupLightButton() { this.setupButtonService('Light', this.cameraConfig.lightButton, eufy_security_client_1.PropertyName.DeviceLight, 'lightbulb'); } async setupChimeButton() { this.setupButtonService('IndoorChime', this.cameraConfig.indoorChimeButton, eufy_security_client_1.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() { var _a, _b, _c, _d, _e; // Find the specific camera config from the platform based on its serial number const foundConfig = (_b = (_a = this.platform.config.cameras) === null || _a === void 0 ? void 0 : _a.find(e => e.serialNumber === this.device.getSerial())) !== null && _b !== void 0 ? _b : {}; // Combine default and specific configurations const config = { ...configTypes_1.DEFAULT_CAMERACONFIG_VALUES, ...foundConfig, name: this.accessory.displayName, }; // Set snapshot handling method based on `forcerefreshsnap` value config.snapshotHandlingMethod = (_c = config.snapshotHandlingMethod) !== null && _c !== void 0 ? _c : (config.forcerefreshsnap ? 1 : 3); // Initialize videoConfig if it's undefined if (!config.videoConfig) { config.videoConfig = {}; } config.videoConfig.debug = (_e = (_d = config.videoConfig) === null || _d === void 0 ? void 0 : _d.debug) !== null && _e !== void 0 ? _e : false; // Validate talkback setting if (config.talkback && !this.device.hasCommand(eufy_security_client_1.CommandName.DeviceStartTalkback)) { this.log.warn(this.accessory.displayName, 'Talkback for this device is not supported!'); config.talkback = false; } // Validate talkback with rtsp setting if (config.talkback && config.rtsp) { this.log.warn(this.accessory.displayName, 'Talkback cannot be used with rtsp option. Ignoring talkback setting.'); config.talkback = false; } this.log.debug(`${this.accessory.displayName} config is`, config); return config; } cameraFunction() { if (!this.cameraConfig.hsv) { this.registerCharacteristic({ serviceType: this.platform.Service.CameraOperatingMode, characteristicType: this.platform.Characteristic.EventSnapshotsActive, getValue: (data) => this.handleDummyEventGet('EventSnapshotsActive'), setValue: (value) => this.handleDummyEventSet('EventSnapshotsActive', value), }); this.registerCharacteristic({ serviceType: this.platform.Service.CameraOperatingMode, characteristicType: this.platform.Characteristic.HomeKitCameraActive, getValue: (data, characteristic) => this.getCameraPropertyValue(characteristic, eufy_security_client_1.PropertyName.DeviceEnabled), setValue: (value, characteristic) => this.setCameraPropertyValue(characteristic, eufy_security_client_1.PropertyName.DeviceEnabled, value), }); if (this.device.hasProperty('enabled')) { this.registerCharacteristic({ serviceType: this.platform.Service.CameraOperatingMode, characteristicType: this.platform.Characteristic.ManuallyDisabled, getValue: (data, characteristic) => this.getCameraPropertyValue(characteristic, eufy_security_client_1.PropertyName.DeviceEnabled), }); } if (this.device.hasProperty('statusLed')) { this.registerCharacteristic({ serviceType: this.platform.Service.CameraOperatingMode, characteristicType: this.platform.Characteristic.CameraOperatingModeIndicator, getValue: (data, characteristic) => this.getCameraPropertyValue(characteristic, eufy_security_client_1.PropertyName.DeviceStatusLed), setValue: (value, characteristic) => this.setCameraPropertyValue(characteristic, eufy_security_client_1.PropertyName.DeviceStatusLed, value), }); } if (this.device.hasProperty('nightvision')) { this.registerCharacteristic({ serviceType: this.platform.Service.CameraOperatingMode, characteristicType: this.platform.Characteristic.NightVision, getValue: (data, characteristic) => this.getCameraPropertyValue(characteristic, eufy_security_client_1.PropertyName.DeviceNightvision), setValue: (value, characteristic) => this.setCameraPropertyValue(characteristic, eufy_security_client_1.PropertyName.DeviceNightvision, value), }); } if (this.device.hasProperty('autoNightvision')) { this.registerCharacteristic({ serviceType: this.platform.Service.CameraOperatingMode, characteristicType: this.platform.Characteristic.NightVision, getValue: (data, characteristic) => this.getCameraPropertyValue(characteristic, eufy_security_client_1.PropertyName.DeviceAutoNightvision), setValue: (value, characteristic) => this.setCameraPropertyValue(characteristic, eufy_security_client_1.PropertyName.DeviceAutoNightvision, value), }); } this.getService(this.platform.Service.CameraOperatingMode).setPrimaryService(true); } // Fire snapshot when motion detected this.registerCharacteristic({ serviceType: this.platform.Service.MotionSensor, characteristicType: this.platform.Characteristic.MotionDetected, getValue: (data) => this.device.getPropertyValue(eufy_security_client_1.PropertyName.DeviceMotionDetected), onValue: (service, characteristic) => { this.eventTypesToHandle.forEach(eventType => { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.device.on(eventType, (device, state) => { // eslint-disable-next-line max-len this.log.info(`${this.accessory.displayName} MOTION DETECTED (${eventType})': ${state}`); characteristic.updateValue(state); }); }); }, }); // if (this.device.hasProperty('speaker')) { // this.registerCharacteristic({ // serviceType: this.platform.Service.Speaker, // characteristicType: this.platform.Characteristic.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: this.platform.Service.Speaker, // characteristicType: this.platform.Characteristic.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: this.platform.Service.Microphone, // characteristicType: this.platform.Characteristic.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: this.platform.Service.Doorbell, characteristicType: this.platform.Characteristic.ProgrammableSwitchEvent, getValue: () => this.handleDummyEventGet('EventSnapshotsActive'), onValue: (service, characteristic) => { this.device.on('rings', (device, state) => 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: this.platform.Service.MotionSensor, characteristicType: this.platform.Characteristic.MotionDetected, getValue: (data) => this.device.getPropertyValue(eufy_security_client_1.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: this.platform.Service.MotionSensor, characteristicType: this.platform.Characteristic.StatusTampered, getValue: (data) => { const tampered = this.device.getPropertyValue(eufy_security_client_1.PropertyName.DeviceEnabled); this.log.debug(`${this.accessory.displayName} TAMPERED? ${!tampered}`); return tampered ? this.platform.Characteristic.StatusTampered.NOT_TAMPERED : this.platform.Characteristic.StatusTampered.TAMPERED; }, }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any getCameraPropertyValue(characteristic, propertyName) { try { let value = this.device.getPropertyValue(propertyName); this.log.debug(`${this.accessory.displayName} GET '${characteristic.displayName}' ${propertyName}: ${value}`); if (propertyName === eufy_security_client_1.PropertyName.DeviceNightvision) { return value === 1; } // Override for PropertyName.DeviceEnabled when enabled button is fired and if (propertyName === eufy_security_client_1.PropertyName.DeviceEnabled && Date.now() - this.cameraStatus.timestamp <= 60000) { // eslint-disable-next-line max-len this.log.debug(`${this.accessory.displayName} CACHED for (1 min) '${characteristic.displayName}' ${propertyName}: ${this.cameraStatus.isEnabled}`); value = this.cameraStatus.isEnabled; } if (characteristic.displayName === 'Manually Disabled') { value = !value; this.log.debug(`${this.accessory.displayName} INVERSED '${characteristic.displayName}' ${propertyName}: ${value}`); } if (value === undefined) { return false; } return value; } catch (error) { this.log.debug(`${this.accessory.displayName} Error getting '${characteristic.displayName}' ${propertyName}: ${error}`); return false; } } // eslint-disable-next-line @typescript-eslint/no-explicit-any async setCameraPropertyValue(characteristic, propertyName, value) { try { this.log.debug(`${this.accessory.displayName} SET '${characteristic.displayName}' ${propertyName}: ${value}`); await this.setPropertyValue(propertyName, value); if (propertyName === eufy_security_client_1.PropertyName.DeviceEnabled && characteristic.displayName === 'On') { characteristic.updateValue(value); this.cameraStatus = { isEnabled: value, timestamp: Date.now() }; characteristic = this.getService(this.platform.Service.CameraOperatingMode) .getCharacteristic(this.platform.Characteristic.ManuallyDisabled); this.log.debug(`${this.accessory.displayName} INVERSED '${characteristic.displayName}' ${propertyName}: ${!value}`); value = !value; } characteristic.updateValue(value); } catch (error) { this.log.debug(`${this.accessory.displayName} 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(`${this.accessory.displayName} DoorBell ringing`); characteristic.updateValue(this.platform.Characteristic.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(channelId) { return -1; } // Set the bitrate for a specific camera channel. async setBitrate(channelId, value) { return true; } // Configure a camera accessory for HomeKit. configureVideoStream() { this.platform.log.debug(`${this.accessory.displayName} StreamingDelegate`); this.streamingDelegate = new streamingDelegate_1.StreamingDelegate(this); this.recordingDelegate = {}; if (this.cameraConfig.hsv) { // If we have smart motion events enabled, let's warn the user that things will not work quite the way they expect. if (this.accessory.getService(this.hap.Service.MotionSensor)) { this.log.info('WARNING: Motion detection and HomeKit Secure Video provide overlapping functionality. ' + 'Only HomeKit Secure Video, when event recording is enabled in the Home app, will be used to trigger motion event notifications for this camera.'); } this.platform.log.debug(`${this.accessory.displayName} RecordingDelegate`); this.recordingDelegate = new recordingDelegate_1.RecordingDelegate(this.streamingDelegate, this.accessory); } this.platform.log.debug(`${this.accessory.displayName} Controller`); const controller = new this.hap.CameraController(this.getCameraControllerOptions()); this.platform.log.debug(`${this.accessory.displayName} streamingDelegate.setController`); this.streamingDelegate.setController(controller); if (this.cameraConfig.hsv) { this.platform.log.debug(`${this.accessory.displayName} recordingDelegate.setController`); this.recordingDelegate.setController(controller); } this.platform.log.debug(`${this.accessory.displayName} configureController`); this.accessory.configureController(controller); this.isVideoConfigured = true; return true; } } exports.CameraAccessory = CameraAccessory; //# sourceMappingURL=CameraAccessory.js.map