homebridge-eufy-security
Version:
Control Eufy Security from homebridge.
506 lines • 24.7 kB
JavaScript
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
;