feat: add cold display alarm flow and labeled snapshots

This commit is contained in:
2026-06-15 12:59:25 +08:00
parent 46889c0621
commit 1059850378
15 changed files with 1164 additions and 15 deletions

View File

@@ -140,6 +140,96 @@ class BatchEngineTests(unittest.TestCase):
self.assertEqual(alarm_events[0]["current_count"], 1)
self.assertIn("alerted_at", alarm_events[0])
def test_pre_warning_emits_once_before_alarm_threshold(self) -> None:
settings = EngineSettings(
camera_id="test_cam",
pre_warning_seconds=60,
max_dwell_seconds=120,
alarm_removal_seconds=30,
trash_confirmation_seconds=30,
zone_ids=("1",),
)
engine = BatchEngine(settings)
engine.process(obs(self.t0, {"1": 1}))
pre_warning_events = engine.process(obs(self.t0 + timedelta(seconds=60), {"1": 1}))
repeated_events = engine.process(obs(self.t0 + timedelta(seconds=90), {"1": 1}))
self.assertEqual([event["event"] for event in pre_warning_events], ["time_pre_warning"])
self.assertEqual(pre_warning_events[0]["severity"], "warning")
self.assertEqual(pre_warning_events[0]["state"], "pre_warning")
self.assertEqual(pre_warning_events[0]["pre_warning_seconds"], 60)
self.assertEqual(pre_warning_events[0]["pre_warned_at"], (self.t0 + timedelta(seconds=60)).isoformat())
self.assertEqual(pre_warning_events[0]["current_count"], 1)
self.assertEqual(repeated_events, [])
def test_pre_warning_removed_before_alarm_is_auto_handled(self) -> None:
settings = EngineSettings(
camera_id="test_cam",
pre_warning_seconds=60,
max_dwell_seconds=120,
alarm_removal_seconds=30,
trash_confirmation_seconds=30,
zone_ids=("1",),
)
engine = BatchEngine(settings)
engine.process(obs(self.t0, {"1": 1}))
engine.process(obs(self.t0 + timedelta(seconds=60), {"1": 1}))
events = engine.process(obs(self.t0 + timedelta(seconds=90), {"1": 0}))
self.assertEqual([event["event"] for event in events], ["pre_warning_handled"])
self.assertEqual(events[0]["severity"], "info")
self.assertEqual(events[0]["state"], "handled")
self.assertEqual(events[0]["handled_source"], "auto_removed_before_alarm")
self.assertEqual(events[0]["ended_at"], (self.t0 + timedelta(seconds=90)).isoformat())
def test_alarm_removal_timeout_emits_once_before_late_removal(self) -> None:
settings = EngineSettings(
camera_id="test_cam",
pre_warning_seconds=60,
max_dwell_seconds=120,
alarm_removal_seconds=30,
trash_confirmation_seconds=30,
zone_ids=("1",),
)
engine = BatchEngine(settings)
engine.process(obs(self.t0, {"1": 1}))
engine.process(obs(self.t0 + timedelta(seconds=60), {"1": 1}))
alarm_events = engine.process(obs(self.t0 + timedelta(seconds=120), {"1": 1}))
timeout_events = engine.process(obs(self.t0 + timedelta(seconds=151), {"1": 1}))
repeated_events = engine.process(obs(self.t0 + timedelta(seconds=160), {"1": 1}))
removal_events = engine.process(obs(self.t0 + timedelta(seconds=170), {"1": 0}, trash=True))
self.assertEqual([event["event"] for event in alarm_events], ["time_alarm"])
self.assertEqual(alarm_events[0]["alarm_removal_deadline"], (self.t0 + timedelta(seconds=150)).isoformat())
self.assertEqual([event["event"] for event in timeout_events], ["alarm_removal_timeout"])
self.assertEqual(timeout_events[0]["severity"], "alarm")
self.assertEqual(timeout_events[0]["state"], "alarm_removal_timeout")
self.assertEqual(timeout_events[0]["reason"], "alarmed_batch_not_removed_after_alarm_window")
self.assertEqual(repeated_events, [])
self.assertEqual([event["event"] for event in removal_events], ["batch_pending_disposal", "batch_discarded"])
def test_alarmed_batch_removed_within_alarm_window_does_not_emit_removal_timeout(self) -> None:
settings = EngineSettings(
camera_id="test_cam",
pre_warning_seconds=60,
max_dwell_seconds=120,
alarm_removal_seconds=30,
trash_confirmation_seconds=30,
zone_ids=("1",),
)
engine = BatchEngine(settings)
engine.process(obs(self.t0, {"1": 1}))
engine.process(obs(self.t0 + timedelta(seconds=60), {"1": 1}))
engine.process(obs(self.t0 + timedelta(seconds=120), {"1": 1}))
events = engine.process(obs(self.t0 + timedelta(seconds=150), {"1": 0}, trash=True))
self.assertEqual([event["event"] for event in events], ["batch_pending_disposal", "batch_discarded"])
self.assertTrue(all(event["event"] != "alarm_removal_timeout" for event in events))
def test_alarmed_batch_removed_without_trash_deposit_escalates_warning(self) -> None:
settings = EngineSettings(
camera_id="test_cam",
@@ -260,6 +350,37 @@ class BatchEngineTests(unittest.TestCase):
self.assertEqual(removal_events[0]["batch_id"], "batch_000124")
self.assertEqual(removal_events[0]["dwell_seconds"], 1400)
def test_restore_keeps_alarm_removal_timeout_deadline_after_runtime_restart(self) -> None:
settings = EngineSettings(
camera_id="test_cam",
pre_warning_seconds=60,
max_dwell_seconds=120,
alarm_removal_seconds=30,
trash_confirmation_seconds=30,
zone_ids=("1",),
)
engine = BatchEngine(settings)
engine.restore_from_events(
[
{
"event": "time_alarm",
"zone_id": "1",
"batch_id": "batch_000124",
"started_at": self.t0.isoformat(),
"pre_warned_at": (self.t0 + timedelta(seconds=60)).isoformat(),
"alerted_at": (self.t0 + timedelta(seconds=120)).isoformat(),
"current_count": 1,
"state": "alerted",
},
],
active_zone_counts={"1": 1},
)
timeout_events = engine.process(obs(self.t0 + timedelta(seconds=151), {"1": 1}))
self.assertEqual([event["event"] for event in timeout_events], ["alarm_removal_timeout"])
self.assertEqual(timeout_events[0]["batch_id"], "batch_000124")
def test_restore_skips_active_false_positive_when_latest_zone_count_is_empty(self) -> None:
settings = EngineSettings(
camera_id="test_cam",