fix(homekit): adjust motion detection threshold and improve hold time checks

This commit is contained in:
Sergey Krashevich
2026-03-05 03:09:02 +03:00
parent 15b0cc4c0c
commit 35fd1383c8
3 changed files with 58 additions and 37 deletions
+1 -1
View File
@@ -142,7 +142,7 @@ homekit:
outdoor: outdoor:
hksv: true hksv: true
motion: detect motion: detect
motion_threshold: 2.0 # P-frame size / baseline ratio to trigger motion (default: 2.0) motion_threshold: 1.0 # P-frame size / baseline ratio to trigger motion (default: 2.0)
``` ```
The `motion_threshold` controls sensitivity. Lower values = more sensitive. Typical values: 1.5 (high sensitivity) to 3.0 (low sensitivity). Default 2.0 works well for most real cameras with static scenes. The `motion_threshold` controls sensitivity. Lower values = more sensitive. Typical values: 1.5 (high sensitivity) to 3.0 (low sensitivity). Default 2.0 works well for most real cameras with static scenes.
+35 -23
View File
@@ -16,6 +16,11 @@ const (
motionAlphaSlow = 0.02 motionAlphaSlow = 0.02
motionHoldTime = 30 * time.Second motionHoldTime = 30 * time.Second
motionCooldown = 5 * time.Second motionCooldown = 5 * time.Second
// check hold time expiry every N frames during active motion (~270ms at 30fps)
motionHoldCheckFrames = 8
// trace log every N frames (~5s at 30fps)
motionTraceFrames = 150
) )
type motionDetector struct { type motionDetector struct {
@@ -33,7 +38,6 @@ type motionDetector struct {
motionActive bool motionActive bool
lastMotion time.Time lastMotion time.Time
lastOff time.Time lastOff time.Time
lastTrace time.Time
// for testing: injectable time and callback // for testing: injectable time and callback
now func() time.Time now func() time.Time
@@ -124,47 +128,55 @@ func (m *motionDetector) handlePacket(packet *rtp.Packet) {
return return
} }
now := m.now() if m.baseline <= 0 {
return
if m.baseline > 0 {
ratio := size / m.baseline
// periodic trace: once per 5 seconds
if now.Sub(m.lastTrace) >= 5*time.Second {
m.lastTrace = now
log.Trace().Str("stream", m.streamName()).
Float64("baseline", m.baseline).Float64("ratio", ratio).
Bool("active", m.motionActive).Msg("[homekit] motion: status")
} }
if ratio > m.threshold { ratio := size / m.baseline
m.lastMotion = now triggered := ratio > m.threshold
if !m.motionActive { if !m.motionActive {
// check cooldown // idle path: check for trigger first, then update baseline
if triggered {
// only call time.Now() when threshold exceeded
now := m.now()
if now.Sub(m.lastOff) >= motionCooldown { if now.Sub(m.lastOff) >= motionCooldown {
m.motionActive = true m.motionActive = true
m.lastMotion = now
log.Debug().Str("stream", m.streamName()).Float64("ratio", ratio).Msg("[homekit] motion: ON") log.Debug().Str("stream", m.streamName()).Float64("ratio", ratio).Msg("[homekit] motion: ON")
m.setMotion(true) m.setMotion(true)
} else { } else {
log.Debug().Str("stream", m.streamName()).Float64("ratio", ratio).Dur("cooldown_left", motionCooldown-now.Sub(m.lastOff)).Msg("[homekit] motion: blocked by cooldown") log.Debug().Str("stream", m.streamName()).Float64("ratio", ratio).
Dur("cooldown_left", motionCooldown-now.Sub(m.lastOff)).Msg("[homekit] motion: blocked by cooldown")
} }
} }
} // update baseline only if still idle (trigger frame doesn't pollute baseline)
}
// update baseline only when no active motion
if !m.motionActive { if !m.motionActive {
m.baseline += motionAlphaSlow * (size - m.baseline) m.baseline += motionAlphaSlow * (size - m.baseline)
} }
} else {
// check hold time expiry // active motion path
if m.motionActive && now.Sub(m.lastMotion) >= motionHoldTime { if triggered {
m.lastMotion = m.now()
} else if m.frameCount%motionHoldCheckFrames == 0 {
// check hold time expiry periodically, not every frame
now := m.now()
if now.Sub(m.lastMotion) >= motionHoldTime {
m.motionActive = false m.motionActive = false
m.lastOff = now m.lastOff = now
log.Debug().Str("stream", m.streamName()).Msg("[homekit] motion: OFF (hold expired)") log.Debug().Str("stream", m.streamName()).Msg("[homekit] motion: OFF (hold expired)")
m.setMotion(false) m.setMotion(false)
} }
} }
}
// periodic trace using frame counter instead of time check
if m.frameCount%motionTraceFrames == 0 {
log.Trace().Str("stream", m.streamName()).
Float64("baseline", m.baseline).Float64("ratio", ratio).
Bool("active", m.motionActive).Msg("[homekit] motion: status")
}
}
func (m *motionDetector) setMotion(detected bool) { func (m *motionDetector) setMotion(detected bool) {
if m.onMotion != nil { if m.onMotion != nil {
+11 -2
View File
@@ -145,7 +145,10 @@ func TestMotionDetector_Cooldown(t *testing.T) {
// trigger and expire motion // trigger and expire motion
det.handlePacket(makePFrame(5000)) det.handlePacket(makePFrame(5000))
clock.advance(motionHoldTime + time.Second) clock.advance(motionHoldTime + time.Second)
det.handlePacket(makePFrame(500)) // trigger hold time check // feed enough small frames to hit a hold check interval
for i := 0; i < motionHoldCheckFrames+1; i++ {
det.handlePacket(makePFrame(500))
}
if len(rec.calls) != 2 || rec.calls[1] != false { if len(rec.calls) != 2 || rec.calls[1] != false {
t.Fatalf("expected ON then OFF, got %v", rec.calls) t.Fatalf("expected ON then OFF, got %v", rec.calls)
} }
@@ -283,7 +286,10 @@ func TestMotionDetector_HoldTimeExtended(t *testing.T) {
// advance past hold time from last trigger // advance past hold time from last trigger
clock.advance(6 * time.Second) clock.advance(6 * time.Second)
// feed enough frames to guarantee hitting hold check interval
for i := 0; i < motionHoldCheckFrames+1; i++ {
det.handlePacket(makePFrame(500)) det.handlePacket(makePFrame(500))
}
last, _ := rec.lastCall() last, _ := rec.lastCall()
if last { if last {
@@ -394,7 +400,10 @@ func TestMotionDetector_MultipleCycles(t *testing.T) {
for cycle := 0; cycle < 3; cycle++ { for cycle := 0; cycle < 3; cycle++ {
det.handlePacket(makePFrame(5000)) det.handlePacket(makePFrame(5000))
clock.advance(motionHoldTime + time.Second) clock.advance(motionHoldTime + time.Second)
det.handlePacket(makePFrame(500)) // trigger OFF // feed enough frames to hit hold check interval
for i := 0; i < motionHoldCheckFrames+1; i++ {
det.handlePacket(makePFrame(500))
}
clock.advance(motionCooldown + time.Second) clock.advance(motionCooldown + time.Second)
} }