feat(aiot): 离岗检测重写 - 单次告警 + 回岗确认 + 持续时长追踪

算法逻辑修改:
- OFF_DUTY状态只告警一次,不再每600秒重复告警
- 人员回岗后需经CONFIRMING(10秒)重新确认才恢复ON_DUTY
- 确认在岗后清除冷却记录,允许新一轮离岗检测
- 非工作时间进入时清除冷却记录

持续时长追踪(新增resolve机制):
- 离岗告警记录alarm_id和leave_start_time
- 人员回岗确认后生成resolve事件(duration_ms + last_frame_time)
- 进入非工作时间时也生成resolve事件
- ResultReporter新增report_alarm_resolve()写入Redis队列
- AlarmUploadWorker新增_process_resolve() HTTP POST到云端
- main.py区分普通告警和resolve事件,回填alarm_id到算法实例
- 告警ext_data附加first_frame_time(离岗开始时间)
This commit is contained in:
2026-02-11 17:55:35 +08:00
parent abcd40f88b
commit ecebdd514f
4 changed files with 127 additions and 18 deletions

View File

@@ -47,6 +47,9 @@ class LeavePostAlgorithm:
self.alarm_sent: bool = False
self.last_person_time: Optional[datetime] = None
self._last_alarm_id: Optional[str] = None # 最近一次告警ID
self._leave_start_time: Optional[datetime] = None # 离岗开始时间LEAVING 状态的开始时间)
def _is_in_working_hours(self, dt: Optional[datetime] = None) -> bool:
if not self.working_hours:
return True
@@ -123,15 +126,28 @@ class LeavePostAlgorithm:
alerts = []
if not in_work:
if self.state == self.STATE_OFF_DUTY and self._last_alarm_id and self._leave_start_time:
duration_ms = int((current_time - self._leave_start_time).total_seconds() * 1000)
alerts.append({
"alert_type": "alarm_resolve",
"resolve_alarm_id": self._last_alarm_id,
"duration_ms": duration_ms,
"last_frame_time": current_time.isoformat(),
"resolve_type": "non_work_time",
})
self._last_alarm_id = None
self._leave_start_time = None
self.state = self.STATE_NON_WORK_TIME
self.detection_history.clear()
self.alarm_sent = False
return []
return alerts
if self.state == self.STATE_NON_WORK_TIME:
self.state = self.STATE_WAITING
self.state_start_time = None
self.detection_history.clear()
self.alarm_sent = False
self.alert_cooldowns.clear() # 新工作时段,清除冷却记录
roi_has_person = False
for det in tracks:
@@ -162,6 +178,8 @@ class LeavePostAlgorithm:
# 持续在岗达到确认时长,正式确认上岗
self.state = self.STATE_ON_DUTY
self.state_start_time = current_time
# 确认在岗后清除冷却记录,允许新一轮离岗检测告警
self.alert_cooldowns.clear()
elif self.state == self.STATE_ON_DUTY:
self.detection_history.append((current_time, roi_has_person))
@@ -178,6 +196,7 @@ class LeavePostAlgorithm:
self.state_start_time = current_time
elif elapsed >= self.confirm_leave_sec:
# 确认离岗后直接触发告警,不再进入 OFF_DUTY 二次等待
leaving_start_time = self.state_start_time # 保存 LEAVING 状态开始时间(人员离开时间)
self.state = self.STATE_OFF_DUTY
self.state_start_time = current_time
@@ -196,30 +215,37 @@ class LeavePostAlgorithm:
})
self.alert_cooldowns[cooldown_key] = now
# 保存告警追踪信息alarm_id 由 main.py 通过 set_last_alarm_id() 回填)
self._last_alarm_id = None
self._leave_start_time = leaving_start_time # LEAVING 状态开始时间 = 人员离开时间
elif self.state == self.STATE_OFF_DUTY:
# OFF_DUTY 状态:等待人员回岗或冷却后可再次告警
# OFF_DUTY 状态:等待人员回岗,不再重复告警
# 必须经过 CONFIRMING 重新确认在岗后,才允许新一轮离岗检测
if roi_has_person:
self.state = self.STATE_ON_DUTY
self.state_start_time = current_time
else:
elapsed = (current_time - self.state_start_time).total_seconds()
cooldown_key = f"{camera_id}_{roi_id}"
now = datetime.now()
if cooldown_key in self.alert_cooldowns and (now - self.alert_cooldowns[cooldown_key]).total_seconds() > self.cooldown_sec:
bbox = self._get_latest_bbox(tracks, roi_id)
elapsed_minutes = int(elapsed / 60)
# 生成 resolve 事件(人员回岗)
if self._last_alarm_id and self._leave_start_time:
duration_ms = int((current_time - self._leave_start_time).total_seconds() * 1000)
alerts.append({
"track_id": roi_id,
"camera_id": camera_id,
"bbox": bbox,
"duration_minutes": elapsed_minutes,
"alert_type": "leave_post",
"message": f"持续离岗 {elapsed_minutes} 分钟",
"alert_type": "alarm_resolve",
"resolve_alarm_id": self._last_alarm_id,
"duration_ms": duration_ms,
"last_frame_time": current_time.isoformat(),
"resolve_type": "person_returned",
})
self.alert_cooldowns[cooldown_key] = now
self._last_alarm_id = None
self._leave_start_time = None
self.state = self.STATE_CONFIRMING
self.state_start_time = current_time
self.detection_history.clear()
self.detection_history.append((current_time, True))
return alerts
def set_last_alarm_id(self, alarm_id: str):
"""由 main.py 在告警生成后回填 alarm_id"""
self._last_alarm_id = alarm_id
def reset(self):
self.state = self.STATE_WAITING
self.state_start_time = None
@@ -227,6 +253,8 @@ class LeavePostAlgorithm:
self.alarm_sent = False
self.last_person_time = None
self.alert_cooldowns.clear()
self._last_alarm_id = None
self._leave_start_time = None
def get_state(self, roi_id: str) -> Dict[str, Any]:
return {