UNPKG

homebridge-eufy-security

Version:
506 lines 24.7 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 configTypes_1 = require("../utils/configTypes"); const utils_1 = require("../utils/utils"); const streamingDelegate_1 = require("../controller/streamingDelegate"); const recordingDelegate_1 = require("../controller/recordingDelegate"); /** * 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 { // 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: utils_1.SERV.Switch, lightbulb: utils_1.SERV.Lightbulb, outlet: utils_1.SERV.Outlet, }; this.registerCharacteristic({ serviceType: platformServiceMapping[serviceType] || utils_1.SERV.Switch, characteristicType: utils_1.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, 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() { // 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 = { ...configTypes_1.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(eufy_security_client_1.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() { if (!this.cameraConfig.hsv) { this.registerCharacteristic({ serviceType: utils_1.SERV.CameraOperatingMode, characteristicType: utils_1.CHAR.EventSnapshotsActive, getValue: () => this.handleDummyEventGet('EventSnapshotsActive'), setValue: (value) => this.handleDummyEventSet('EventSnapshotsActive', value), }); this.registerCharacteristic({ serviceType: utils_1.SERV.CameraOperatingMode, characteristicType: utils_1.CHAR.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), onValue: (service, characteristic) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.device.on('property changed', (device, name, value) => { this.applyPropertyValue(characteristic, eufy_security_client_1.PropertyName.DeviceEnabled, value); }); }, }); if (this.device.hasProperty('enabled')) { this.registerCharacteristic({ serviceType: utils_1.SERV.CameraOperatingMode, characteristicType: utils_1.CHAR.ManuallyDisabled, getValue: (data, characteristic) => this.getCameraPropertyValue(characteristic, eufy_security_client_1.PropertyName.DeviceEnabled), onValue: (service, characteristic) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.device.on('property changed', (device, name, value) => { this.applyPropertyValue(characteristic, eufy_security_client_1.PropertyName.DeviceEnabled, value); }); }, }); } if (this.device.hasProperty('statusLed')) { this.registerCharacteristic({ serviceType: utils_1.SERV.CameraOperatingMode, characteristicType: utils_1.CHAR.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), onValue: (service, characteristic) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.device.on('property changed', (device, name, value) => { this.applyPropertyValue(characteristic, eufy_security_client_1.PropertyName.DeviceStatusLed, value); }); }, }); } if (this.device.hasProperty('nightvision')) { this.registerCharacteristic({ serviceType: utils_1.SERV.CameraOperatingMode, characteristicType: utils_1.CHAR.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), onValue: (service, characteristic) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.device.on('property changed', (device, name, value) => { this.applyPropertyValue(characteristic, eufy_security_client_1.PropertyName.DeviceNightvision, value); }); }, }); } if (this.device.hasProperty('autoNightvision')) { this.registerCharacteristic({ serviceType: utils_1.SERV.CameraOperatingMode, characteristicType: utils_1.CHAR.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), onValue: (service, characteristic) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.device.on('property changed', (device, name, value) => { this.applyPropertyValue(characteristic, eufy_security_client_1.PropertyName.DeviceAutoNightvision, value); }); }, }); } this.getService(utils_1.SERV.CameraOperatingMode).setPrimaryService(true); } // Fire snapshot when motion detected this.registerCharacteristic({ serviceType: utils_1.SERV.MotionSensor, characteristicType: utils_1.CHAR.MotionDetected, getValue: () => 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) => { 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: utils_1.SERV.Doorbell, characteristicType: utils_1.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: utils_1.SERV.MotionSensor, characteristicType: utils_1.CHAR.MotionDetected, getValue: () => 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: utils_1.SERV.MotionSensor, characteristicType: utils_1.CHAR.StatusTampered, getValue: () => { const tampered = this.device.getPropertyValue(eufy_security_client_1.PropertyName.DeviceEnabled); this.log.debug(`TAMPERED? ${!tampered}`); return tampered ? utils_1.CHAR.StatusTampered.NOT_TAMPERED : utils_1.CHAR.StatusTampered.TAMPERED; }, }); if (this.device.isDoorbell()) { this.registerCharacteristic({ serviceType: utils_1.SERV.Doorbell, characteristicType: utils_1.CHAR.ProgrammableSwitchEvent, onValue: (service, characteristic) => { this.device.on('rings', () => this.onDeviceRingsPushNotification(characteristic)); }, }); } } // eslint-disable-next-line @typescript-eslint/no-explicit-any 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; } } // eslint-disable-next-line @typescript-eslint/no-explicit-any applyPropertyValue(characteristic, propertyName, value) { this.log.debug(`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) { 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; } // eslint-disable-next-line @typescript-eslint/no-explicit-any async setCameraPropertyValue(characteristic, propertyName, value) { try { this.log.debug(`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(utils_1.SERV.CameraOperatingMode) .getCharacteristic(utils_1.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(utils_1.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_1.StreamingDelegate(this); this.log.debug(`RecordingDelegate`); this.recordingDelegate = new recordingDelegate_1.RecordingDelegate(this.platform, this.accessory, this.device, this.cameraConfig, this.streamingDelegate.getLivestreamManager()); this.log.debug(`Controller`); const controller = new this.platform.api.hap.CameraController(this.getCameraControllerOptions()); this.log.debug(`streamingDelegate.setController`); this.streamingDelegate.setController(controller); if (this.cameraConfig.hsv) { this.log.debug(`recordingDelegate.setController`); this.recordingDelegate.setController(controller); } this.log.debug(`configureController`); 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: this.cameraConfig.hsv ? { 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, } : undefined, sensors: this.cameraConfig.hsv ? { motion: this.getService(utils_1.SERV.MotionSensor), occupancy: undefined, } : undefined, }; return option; } } exports.CameraAccessory = CameraAccessory; //# sourceMappingURL=CameraAccessory.js.map