homebridge-loxone-proxy
Version:
Homebridge Dynamic Platform Plugin which exposes a Loxone System to Homekit.
179 lines • 6.86 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CameraMotionSensor = void 0;
const BaseService_1 = require("./BaseService");
class CameraMotionSensor extends BaseService_1.BaseService {
constructor(platform, accessory, camera, doorbellService) {
var _a, _b, _c;
super(platform, accessory);
this.camera = camera;
this.doorbellService = doorbellService;
this.intervalMs = 1000;
this.minThreshold = 0.04;
this.maxThreshold = 0.30;
this.minDeltaBytes = 1500;
this.cooldown = 8000;
this.resetTimeout = 15000;
this.historyLimit = 20;
this.minimumHistory = 5;
this.snapshotHistory = [];
this.snapshotFailures = 0;
this.lastTrigger = 0;
this.polling = false;
this.shuttingDown = false;
this.state = { MotionDetected: false };
this.jpegHeaderSize = (_c = (_b = (_a = platform.config) === null || _a === void 0 ? void 0 : _a.Advanced) === null || _b === void 0 ? void 0 : _b.JpegHeaderSize) !== null && _c !== void 0 ? _c : 623;
this.setupService();
this.startPolling();
platform.api.on("shutdown", () => {
this.shuttingDown = true;
if (this.loopTimer) {
clearTimeout(this.loopTimer);
this.loopTimer = undefined;
}
if (this.resetTimer) {
clearTimeout(this.resetTimer);
this.resetTimer = undefined;
}
this.platform.log.debug(`[${this.accessory.displayName}] Motion detection stopped (shutdown)`);
});
}
setupService() {
this.service =
this.accessory.getService(this.platform.Service.MotionSensor) ||
this.accessory.addService(this.platform.Service.MotionSensor);
this.service
.getCharacteristic(this.platform.Characteristic.MotionDetected)
.onGet(() => this.state.MotionDetected);
}
startPolling() {
this.scheduleNextPoll(0);
}
scheduleNextPoll(delayMs) {
if (this.shuttingDown) {
return;
}
if (this.loopTimer) {
clearTimeout(this.loopTimer);
}
this.loopTimer = setTimeout(() => {
void this.pollOnce();
}, delayMs);
}
async pollOnce() {
if (this.shuttingDown || this.polling) {
return;
}
this.polling = true;
let nextDelay = this.intervalMs;
try {
const size = await this.readSnapshotSize();
if (size !== null) {
this.snapshotFailures = 0;
const now = Date.now();
if (this.evaluateMotion(size, now)) {
this.triggerMotion(now);
}
}
else {
nextDelay = this.handleSnapshotFailure();
}
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.platform.log.debug(`[${this.accessory.displayName}] Motion polling error: ${message}`);
nextDelay = this.handleSnapshotFailure();
}
finally {
this.polling = false;
this.scheduleNextPoll(nextDelay);
}
}
async readSnapshotSize() {
const headerSize = this.jpegHeaderSize;
const headerSizeResult = await this.camera.getSnapshotSize();
if (headerSizeResult !== null) {
return Math.max(0, headerSizeResult - headerSize);
}
const snapshot = await this.camera.getSnapshot(false);
if (!snapshot) {
return null;
}
return Math.max(0, snapshot.length - headerSize);
}
handleSnapshotFailure() {
if (this.snapshotFailures < 3) {
this.platform.log.warn(`[${this.accessory.displayName}] Snapshot unavailable`);
}
this.snapshotFailures++;
return Math.min(this.intervalMs * 2 ** this.snapshotFailures, 60000);
}
evaluateMotion(current, now) {
this.snapshotHistory.push(current);
if (this.snapshotHistory.length > this.historyLimit) {
this.snapshotHistory.shift();
}
if (this.snapshotHistory.length < this.minimumHistory) {
return false;
}
const baseline = this.median(this.snapshotHistory.slice(0, -1));
if (baseline <= 0) {
return false;
}
const deltaAbs = Math.abs(current - baseline);
const deltaRel = deltaAbs / baseline;
return (now - this.lastTrigger > this.cooldown &&
deltaRel > this.minThreshold &&
deltaRel < this.maxThreshold &&
deltaAbs > this.minDeltaBytes);
}
triggerMotion(now) {
var _a;
if (!this.state.MotionDetected) {
this.platform.log.info(`[${this.accessory.displayName}] Motion detected`);
this.state.MotionDetected = true;
(_a = this.service) === null || _a === void 0 ? void 0 : _a.updateCharacteristic(this.platform.Characteristic.MotionDetected, true);
this.triggerDoorbellFromMotion();
}
if (this.resetTimer) {
clearTimeout(this.resetTimer);
}
this.resetTimer = setTimeout(() => this.resetMotion(), this.resetTimeout);
this.lastTrigger = now;
}
triggerDoorbellFromMotion() {
var _a, _b, _c;
if (!((_b = (_a = this.platform.config) === null || _a === void 0 ? void 0 : _a.Advanced) === null || _b === void 0 ? void 0 : _b.MotionTriggersDoorbell)) {
return;
}
try {
(_c = this.doorbellService) === null || _c === void 0 ? void 0 : _c.triggerDoorbell();
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.platform.log.warn(`[${this.accessory.displayName}] Failed to trigger doorbell from motion: ${message}`);
}
}
resetMotion() {
var _a;
if (!this.state.MotionDetected) {
return;
}
this.platform.log.info(`[${this.accessory.displayName}] Motion ended`);
this.state.MotionDetected = false;
(_a = this.service) === null || _a === void 0 ? void 0 : _a.updateCharacteristic(this.platform.Characteristic.MotionDetected, false);
this.resetTimer = undefined;
}
median(values) {
if (!values.length) {
return 0;
}
const sorted = values.slice().sort((a, b) => a - b);
const mid = sorted.length >> 1;
return sorted.length & 1
? sorted[mid]
: (sorted[mid - 1] + sorted[mid]) / 2;
}
}
exports.CameraMotionSensor = CameraMotionSensor;
//# sourceMappingURL=CameraMotionSensor.js.map