182 lines
5.7 KiB
Python
182 lines
5.7 KiB
Python
"""预警发布器 (MVP-2 / D18)
|
|
|
|
将统一 ``AlertEvent`` 格式化为前端/外部系统可消费的 JSON 消息,
|
|
并通过 ``MQTTService`` 发布到对应主题。
|
|
|
|
主题命名规则::
|
|
|
|
{prefix}/{event_type}/{source_id} # 单流单类型
|
|
{prefix}/all # 全量订阅
|
|
{prefix}/{event_type} # 按事件类型订阅
|
|
|
|
消息格式 (与前端约定)::
|
|
|
|
{
|
|
"alert_id": "...",
|
|
"event_type": "fire",
|
|
"severity": "critical",
|
|
"confidence": 0.92,
|
|
"source_id": "cam-01",
|
|
"rule_name": "fire_critical",
|
|
"first_seen": 1781233417.12,
|
|
"last_seen": 1781233418.45,
|
|
"occurrence_count": 3,
|
|
"detections": [{...}],
|
|
"metadata": {...}
|
|
}
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
from typing import Any, Dict, Iterable, List, Optional
|
|
|
|
from models.event_schemas import AlertEvent
|
|
from .mqtt_service import MQTTService
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# AlertPublisher
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class AlertPublisher:
|
|
"""预警事件发布器。
|
|
|
|
Args:
|
|
mqtt_service: MQTT 服务实例
|
|
topic_prefix: 主题前缀
|
|
broadcast_all: 是否同时发布到 ``{prefix}/all`` 总订阅主题
|
|
qos: 发布 QoS 等级
|
|
retain: 是否保留最后一条预警 (用于新订阅者立即获取最近状态)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
mqtt_service: MQTTService,
|
|
topic_prefix: str = "video/alerts",
|
|
broadcast_all: bool = True,
|
|
qos: int = 1,
|
|
retain: bool = False,
|
|
) -> None:
|
|
self._mqtt = mqtt_service
|
|
self.topic_prefix = topic_prefix.rstrip("/")
|
|
self.broadcast_all = broadcast_all
|
|
self.qos = qos
|
|
self.retain = retain
|
|
self._published_count = 0
|
|
self._failed_count = 0
|
|
|
|
# ------------------------------------------------------------------
|
|
# 发布
|
|
# ------------------------------------------------------------------
|
|
|
|
async def publish_alert(self, alert: AlertEvent) -> bool:
|
|
"""发布单条预警事件。
|
|
|
|
Returns:
|
|
True 表示至少有一条主题发布成功
|
|
"""
|
|
|
|
payload = self.format_alert(alert)
|
|
topics = self._build_topics(alert)
|
|
|
|
any_success = False
|
|
for topic in topics:
|
|
ok = await self._mqtt.publish(
|
|
topic, payload, qos=self.qos, retain=self.retain
|
|
)
|
|
if ok:
|
|
any_success = True
|
|
self._published_count += 1
|
|
logger.debug("AlertPublisher 已发布 topic=%s alert_id=%s", topic, alert.alert_id)
|
|
else:
|
|
self._failed_count += 1
|
|
logger.warning(
|
|
"AlertPublisher 发布失败 topic=%s alert_id=%s", topic, alert.alert_id
|
|
)
|
|
return any_success
|
|
|
|
async def publish_alerts(self, alerts: Iterable[AlertEvent]) -> int:
|
|
"""批量发布预警事件,返回成功条数。"""
|
|
|
|
success = 0
|
|
for alert in alerts:
|
|
if await self.publish_alert(alert):
|
|
success += 1
|
|
return success
|
|
|
|
# ------------------------------------------------------------------
|
|
# 格式化
|
|
# ------------------------------------------------------------------
|
|
|
|
@staticmethod
|
|
def format_alert(alert: AlertEvent) -> Dict[str, Any]:
|
|
"""将 AlertEvent 格式化为 dict (JSON 友好)。"""
|
|
|
|
return {
|
|
"alert_id": alert.alert_id,
|
|
"event_type": alert.event_type.value,
|
|
"severity": alert.severity.value,
|
|
"confidence": round(alert.confidence, 4),
|
|
"source_id": alert.source_id,
|
|
"rule_name": alert.rule_name,
|
|
"first_seen": alert.first_seen,
|
|
"last_seen": alert.last_seen,
|
|
"occurrence_count": alert.occurrence_count,
|
|
"detections": [
|
|
{
|
|
"detection_id": d.detection_id,
|
|
"track_id": d.track_id,
|
|
"class_name": d.class_name,
|
|
"label": d.label,
|
|
"confidence": round(d.confidence, 4),
|
|
"bbox": d.bbox.to_list(),
|
|
"source": d.source.value,
|
|
"model_id": d.model_id,
|
|
"timestamp": d.timestamp,
|
|
}
|
|
for d in alert.detections
|
|
],
|
|
"metadata": alert.metadata,
|
|
"published_at": time.time(),
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# 主题构建
|
|
# ------------------------------------------------------------------
|
|
|
|
def _build_topics(self, alert: AlertEvent) -> List[str]:
|
|
topics: List[str] = []
|
|
event_type = alert.event_type.value
|
|
source_id = alert.source_id or "unknown"
|
|
|
|
# 细粒度主题
|
|
topics.append(f"{self.topic_prefix}/{event_type}/{source_id}")
|
|
# 事件类型订阅
|
|
topics.append(f"{self.topic_prefix}/{event_type}")
|
|
# 总广播
|
|
if self.broadcast_all:
|
|
topics.append(f"{self.topic_prefix}/all")
|
|
|
|
return topics
|
|
|
|
# ------------------------------------------------------------------
|
|
# 状态
|
|
# ------------------------------------------------------------------
|
|
|
|
@property
|
|
def stats(self) -> Dict[str, Any]:
|
|
return {
|
|
"topic_prefix": self.topic_prefix,
|
|
"published_count": self._published_count,
|
|
"failed_count": self._failed_count,
|
|
"mqtt_connected": self._mqtt.is_connected,
|
|
}
|
|
|
|
|
|
__all__ = ["AlertPublisher"]
|