edge/engine.py _mk hardcoded duration_s=0.0 for every event. The §7.3 wire contract / P1-B dashboard expects the real incident duration for no_motion, immobility, and ghost events. _mk now derives duration_s from the metrics dict: immobility_s (fall no_motion, immobility watchdog) or waited_s (ghost), falling back to 0.0 for instantaneous confirms (fall, zone). Tests: tests/test_engine.py adds test_no_motion_event_carries_duration, driving a track to NO_MOTION and asserting the emitted event's duration_s is > 0 and equals metrics["immobility_s"].
79 lines
3.2 KiB
Python
79 lines
3.2 KiB
Python
from collections import deque
|
|
from edge.models import Keypoint, BBox
|
|
from edge.tracking import Track, TrackFrame
|
|
from edge.events import EventType
|
|
from edge.detectors.fall import FallDetector, FallConfig
|
|
from edge.detectors.immobility import ImmobilityWatchdog, ImmobilityConfig
|
|
from edge.detectors.ghost import GhostIncidentDetector, GhostConfig
|
|
from edge.detectors.zone import ZoneIntrusionDetector, ZoneConfig
|
|
from edge.engine import EventEngine, EngineConfig
|
|
|
|
def _lying_kps():
|
|
kps = [Keypoint(150, 300, 0.9) for _ in range(17)]
|
|
kps[5] = Keypoint(100, 300, 0.9); kps[6] = Keypoint(100, 310, 0.9)
|
|
kps[11] = Keypoint(200, 300, 0.9); kps[12] = Keypoint(200, 310, 0.9)
|
|
kps[0] = Keypoint(90, 320, 0.9)
|
|
return kps
|
|
|
|
def _lying_track(track, t):
|
|
track.frames.append(TrackFrame(t, BBox(90, 290, 210, 320), _lying_kps()))
|
|
return track
|
|
|
|
def _engine():
|
|
return EventEngine(
|
|
FallDetector(FallConfig(prone_confirm_s=0.5)),
|
|
ImmobilityWatchdog(ImmobilityConfig(immobile_s=999)),
|
|
GhostIncidentDetector(GhostConfig()),
|
|
ZoneIntrusionDetector([], ZoneConfig()),
|
|
EngineConfig(camera_id="cam-01", cooldown_s=30.0),
|
|
)
|
|
|
|
def test_fall_produces_fall_event():
|
|
eng = _engine()
|
|
tr = Track(1, deque(maxlen=90))
|
|
_lying_track(tr, 0.0)
|
|
eng.process([tr], [], now=0.0)
|
|
_lying_track(tr, 0.6)
|
|
events = eng.process([tr], [], now=0.6)
|
|
types = [e.event_type for e in events]
|
|
assert EventType.FALL in types
|
|
assert events[0].camera_id == "cam-01"
|
|
|
|
def test_cooldown_suppresses_duplicate():
|
|
eng = _engine()
|
|
tr = Track(1, deque(maxlen=90))
|
|
_lying_track(tr, 0.0); eng.process([tr], [], now=0.0)
|
|
_lying_track(tr, 0.6); eng.process([tr], [], now=0.6) # FALL 1회
|
|
_lying_track(tr, 0.7)
|
|
again = eng.process([tr], [], now=0.7)
|
|
assert all(e.event_type != EventType.FALL for e in again)
|
|
|
|
def test_no_motion_event_carries_duration():
|
|
eng = EventEngine(
|
|
FallDetector(FallConfig(prone_confirm_s=0.5, no_motion_s=1.0, motion_thr=0.05)),
|
|
ImmobilityWatchdog(ImmobilityConfig(immobile_s=999)),
|
|
GhostIncidentDetector(GhostConfig()),
|
|
ZoneIntrusionDetector([], ZoneConfig()),
|
|
EngineConfig(camera_id="cam-01", cooldown_s=30.0),
|
|
)
|
|
tr = Track(1, deque(maxlen=90))
|
|
_lying_track(tr, 0.0); eng.process([tr], [], now=0.0)
|
|
_lying_track(tr, 0.6); eng.process([tr], [], now=0.6) # -> fall
|
|
_lying_track(tr, 1.0); eng.process([tr], [], now=1.0) # still
|
|
_lying_track(tr, 2.0)
|
|
events = eng.process([tr], [], now=2.0) # immobile >1.0s -> no_motion
|
|
no_motion_events = [e for e in events if e.event_type == EventType.NO_MOTION]
|
|
assert len(no_motion_events) == 1
|
|
e = no_motion_events[0]
|
|
assert e.duration_s > 0
|
|
assert e.duration_s == e.metrics["immobility_s"]
|
|
|
|
|
|
def test_allow_cooldown_primitive():
|
|
eng = _engine() # EngineConfig(camera_id="cam-01", cooldown_s=30.0)
|
|
key = ("FALL", 1)
|
|
assert eng._allow(key, now=0.0) is True # first time allowed
|
|
assert eng._allow(key, now=5.0) is False # within cooldown -> suppressed
|
|
assert eng._allow(key, now=40.0) is True # after cooldown_s elapsed -> allowed again
|
|
# a different key is independent
|
|
assert eng._allow(("FALL", 2), now=5.0) is True
|