fix(homekit): adjust motion detection threshold and improve hold time checks
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
+45
-33
@@ -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,45 +128,53 @@ 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
|
||||||
ratio := size / m.baseline
|
triggered := ratio > m.threshold
|
||||||
|
|
||||||
// periodic trace: once per 5 seconds
|
if !m.motionActive {
|
||||||
if now.Sub(m.lastTrace) >= 5*time.Second {
|
// idle path: check for trigger first, then update baseline
|
||||||
m.lastTrace = now
|
if triggered {
|
||||||
log.Trace().Str("stream", m.streamName()).
|
// only call time.Now() when threshold exceeded
|
||||||
Float64("baseline", m.baseline).Float64("ratio", ratio).
|
now := m.now()
|
||||||
Bool("active", m.motionActive).Msg("[homekit] motion: status")
|
if now.Sub(m.lastOff) >= motionCooldown {
|
||||||
|
m.motionActive = true
|
||||||
|
m.lastMotion = now
|
||||||
|
log.Debug().Str("stream", m.streamName()).Float64("ratio", ratio).Msg("[homekit] motion: ON")
|
||||||
|
m.setMotion(true)
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
if ratio > m.threshold {
|
if !m.motionActive {
|
||||||
m.lastMotion = now
|
m.baseline += motionAlphaSlow * (size - m.baseline)
|
||||||
if !m.motionActive {
|
}
|
||||||
// check cooldown
|
} else {
|
||||||
if now.Sub(m.lastOff) >= motionCooldown {
|
// active motion path
|
||||||
m.motionActive = true
|
if triggered {
|
||||||
log.Debug().Str("stream", m.streamName()).Float64("ratio", ratio).Msg("[homekit] motion: ON")
|
m.lastMotion = m.now()
|
||||||
m.setMotion(true)
|
} else if m.frameCount%motionHoldCheckFrames == 0 {
|
||||||
} else {
|
// check hold time expiry periodically, not every frame
|
||||||
log.Debug().Str("stream", m.streamName()).Float64("ratio", ratio).Dur("cooldown_left", motionCooldown-now.Sub(m.lastOff)).Msg("[homekit] motion: blocked by cooldown")
|
now := m.now()
|
||||||
}
|
if now.Sub(m.lastMotion) >= motionHoldTime {
|
||||||
|
m.motionActive = false
|
||||||
|
m.lastOff = now
|
||||||
|
log.Debug().Str("stream", m.streamName()).Msg("[homekit] motion: OFF (hold expired)")
|
||||||
|
m.setMotion(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update baseline only when no active motion
|
// periodic trace using frame counter instead of time check
|
||||||
if !m.motionActive {
|
if m.frameCount%motionTraceFrames == 0 {
|
||||||
m.baseline += motionAlphaSlow * (size - m.baseline)
|
log.Trace().Str("stream", m.streamName()).
|
||||||
}
|
Float64("baseline", m.baseline).Float64("ratio", ratio).
|
||||||
|
Bool("active", m.motionActive).Msg("[homekit] motion: status")
|
||||||
// check hold time expiry
|
|
||||||
if m.motionActive && now.Sub(m.lastMotion) >= motionHoldTime {
|
|
||||||
m.motionActive = false
|
|
||||||
m.lastOff = now
|
|
||||||
log.Debug().Str("stream", m.streamName()).Msg("[homekit] motion: OFF (hold expired)")
|
|
||||||
m.setMotion(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
det.handlePacket(makePFrame(500))
|
// feed enough frames to guarantee hitting hold check interval
|
||||||
|
for i := 0; i < motionHoldCheckFrames+1; i++ {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user