homebridge-tapo-camera
Version:
Homebridge plugin for TP-Link TAPO security cameras
216 lines • 9.98 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CameraAccessory = void 0;
const streamingDelegate_1 = require("homebridge-camera-ffmpeg/dist/streamingDelegate");
const logger_1 = require("homebridge-camera-ffmpeg/dist/logger");
const tapoCamera_1 = require("./tapoCamera");
const pkg_1 = require("./pkg");
class CameraAccessory {
platform;
config;
log;
api;
camera;
pullIntervalTick;
accessory;
infoAccessory;
toggleAccessories = {};
cachedStatus = {};
motionSensorService;
randomSeed = Math.random();
constructor(platform, config) {
this.platform = platform;
this.config = config;
// @ts-expect-error - private property
this.log = {
...this.platform.log,
prefix: this.platform.log.prefix + `/${this.config.name}`,
};
this.api = this.platform.api;
this.accessory = new this.api.platformAccessory(this.config.name, this.api.hap.uuid.generate(this.config.name), 17 /* this.api.hap.Categories.CAMERA */);
this.camera = new tapoCamera_1.TAPOCamera(this.log, this.config);
}
setupInfoAccessory(basicInfo) {
this.infoAccessory =
this.accessory.getService(this.api.hap.Service.AccessoryInformation) ||
this.accessory.addService(this.api.hap.Service.AccessoryInformation);
this.infoAccessory
.setCharacteristic(this.api.hap.Characteristic.Manufacturer, "TAPO")
.setCharacteristic(this.api.hap.Characteristic.Model, basicInfo.device_info)
.setCharacteristic(this.api.hap.Characteristic.SerialNumber, basicInfo.mac)
.setCharacteristic(this.api.hap.Characteristic.FirmwareRevision, basicInfo.sw_version);
}
setupToggleAccessory(name, tapoServiceStr) {
try {
const toggleService = this.accessory.addService(this.api.hap.Service.Switch, name, tapoServiceStr);
this.toggleAccessories[tapoServiceStr] = toggleService;
toggleService.addOptionalCharacteristic(this.api.hap.Characteristic.ConfiguredName);
toggleService.setCharacteristic(this.api.hap.Characteristic.ConfiguredName, name);
toggleService
.getCharacteristic(this.api.hap.Characteristic.On)
.onGet(async () => {
try {
this.log.debug(`Getting "${tapoServiceStr}" status...`);
const cachedValue = this.cachedStatus[tapoServiceStr];
if (cachedValue !== undefined) {
return cachedValue;
}
const currentValue = toggleService.getCharacteristic(this.api.hap.Characteristic.On).value;
void this.getStatusAndNotify();
if (typeof currentValue === "boolean") {
this.log.debug(`No cached status for "${tapoServiceStr}", returning Homebridge cached value`);
return currentValue;
}
this.log.debug(`No cached status for "${tapoServiceStr}", returning fallback value`);
return false;
}
catch (err) {
this.log.error("Error getting status:", err);
return false;
}
})
.onSet(async (newValue) => {
try {
const value = Boolean(newValue);
this.log.debug(`Setting "${tapoServiceStr}" to ${value ? "on" : "off"}...`);
await this.camera.setStatus(tapoServiceStr, value);
this.cachedStatus[tapoServiceStr] = value;
toggleService
.getCharacteristic(this.api.hap.Characteristic.On)
.updateValue(value);
}
catch (err) {
this.log.error("Error setting status:", err);
throw new this.api.hap.HapStatusError(-70409 /* this.api.hap.HAPStatus.RESOURCE_DOES_NOT_EXIST */);
}
});
}
catch (err) {
this.log.error("Error setting up toggle accessory", name, tapoServiceStr, err);
}
}
getVideoConfig() {
const streamUrl = this.camera.getAuthenticatedStreamUrl(Boolean(this.config.lowQuality));
const vcodec = this.config.videoCodec ?? "copy";
const config = {
audio: true, // Set audio as true as most of TAPO cameras have audio
vcodec: vcodec,
maxWidth: this.config.videoMaxWidth,
maxHeight: this.config.videoMaxHeight,
maxFPS: this.config.videoMaxFPS,
maxBitrate: this.config.videoMaxBirate,
packetSize: this.config.videoPacketSize,
forceMax: this.config.videoForceMax,
...(this.config.videoConfig || {}),
// We add this at the end as the user most not be able to override it
source: `-i ${streamUrl}`,
};
this.log.debug("Video config", config);
return config;
}
async setupCameraStreaming(basicInfo) {
try {
const delegate = new streamingDelegate_1.StreamingDelegate(new logger_1.Logger(this.log), {
name: this.config.name,
manufacturer: "TAPO",
model: basicInfo.device_info,
serialNumber: basicInfo.mac,
firmwareRevision: basicInfo.sw_version,
unbridge: true,
videoConfig: this.getVideoConfig(),
}, this.api, this.api.hap);
this.accessory.configureController(delegate.controller);
this.log.debug("Camera streaming setup done");
}
catch (err) {
this.log.error("Error setting up camera streaming:", err);
}
}
async setupMotionSensorAccessory() {
try {
this.motionSensorService = this.accessory.addService(this.platform.api.hap.Service.MotionSensor, "Motion Sensor", "motion");
this.motionSensorService.addOptionalCharacteristic(this.api.hap.Characteristic.ConfiguredName);
this.motionSensorService.setCharacteristic(this.api.hap.Characteristic.ConfiguredName, "Motion Sensor");
const eventEmitter = await this.camera.getEventEmitter();
eventEmitter.addListener("motion", (motionDetected) => {
this.log.debug("Motion detected", motionDetected);
this.motionSensorService?.updateCharacteristic(this.api.hap.Characteristic.MotionDetected, motionDetected);
});
}
catch (err) {
this.log.error("Error setting up motion sensor accessory:", err);
}
}
setupPolling() {
if (this.pullIntervalTick) {
clearInterval(this.pullIntervalTick);
}
this.pullIntervalTick = setInterval(() => {
this.log.debug("Polling status...");
this.getStatusAndNotify();
}, this.config.pullInterval || this.platform.kDefaultPullInterval);
}
async getStatusAndNotify() {
try {
const cameraStatus = await this.camera.getStatus();
this.cachedStatus = {
...this.cachedStatus,
...cameraStatus,
};
this.log.debug("Notifying new values...", cameraStatus);
for (const [key, value] of Object.entries(cameraStatus)) {
const toggleService = this.toggleAccessories[key];
if (toggleService && value !== undefined) {
toggleService
.getCharacteristic(this.api.hap.Characteristic.On)
.updateValue(value);
}
}
}
catch (err) {
this.log.error("Error getting status:", err);
}
}
async setup() {
const basicInfo = await this.camera.getBasicInfo();
this.log.debug("Basic info", basicInfo);
this.accessory.on("identify" /* PlatformAccessoryEvent.IDENTIFY */, () => {
this.log.info("Identify requested", basicInfo);
});
this.setupInfoAccessory(basicInfo);
if (!this.config.disableStreaming) {
this.setupCameraStreaming(basicInfo);
}
if (!this.config.disableEyesToggleAccessory) {
this.setupToggleAccessory(this.config.eyesToggleAccessoryName || "Eyes", "eyes");
}
if (!this.config.disableAlarmToggleAccessory) {
this.setupToggleAccessory(this.config.alarmToggleAccessoryName || "Alarm", "alarm");
}
if (!this.config.disableNotificationsToggleAccessory) {
this.setupToggleAccessory(this.config.notificationsToggleAccessoryName || "Notifications", "notifications");
}
if (!this.config.disableMotionDetectionToggleAccessory) {
this.setupToggleAccessory(this.config.motionDetectionToggleAccessoryName || "Motion Detection", "motionDetection");
}
if (!this.config.disableLEDToggleAccessory) {
this.setupToggleAccessory(this.config.ledToggleAccessoryName || "LED", "led");
}
if (!this.config.disableMotionSensorAccessory) {
this.setupMotionSensorAccessory();
}
// // Publish as external accessory
this.log.debug("Publishing accessory...");
this.api.publishExternalAccessories(pkg_1.PLUGIN_ID, [this.accessory]);
// Setup the polling by giving a random delay
// to avoid all the cameras starting at the same time
this.log.debug("Setting up polling...");
setTimeout(() => {
this.setupPolling();
}, this.randomSeed * 3_000);
this.log.debug("Notifying initial values...");
await this.getStatusAndNotify();
}
}
exports.CameraAccessory = CameraAccessory;
//# sourceMappingURL=cameraAccessory.js.map