feat(server): 新增 MQTT 预警消息发布服务
This commit is contained in:
181
apps/server/services/alert_publisher.py
Normal file
181
apps/server/services/alert_publisher.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""预警发布器 (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"]
|
||||
Reference in New Issue
Block a user