Files
security-ai-edge/core/result_reporter.py

235 lines
7.6 KiB
Python
Raw Normal View History

2026-01-29 18:33:12 +08:00
"""
结果上报模块
使用本地 Redis 缓冲 + HTTP 上报替代 MQTT
告警流程
算法产出告警 report_alarm() LPUSH Redis AlarmUploadWorker 异步消费
2026-01-29 18:33:12 +08:00
"""
import json
import logging
import uuid
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
from dataclasses import dataclass, field
2026-01-29 18:33:12 +08:00
import numpy as np
import redis
from config.settings import get_settings
2026-01-29 18:33:12 +08:00
logger = logging.getLogger(__name__)
@dataclass
class AlarmInfo:
"""告警信息类(新格式,对齐云端 alarm_event 表)"""
alarm_id: str
alarm_type: str
device_id: str
scene_id: str
event_time: str # ISO8601
alarm_level: int # 1-4
snapshot_local_path: Optional[str] = None
algorithm_code: Optional[str] = None
confidence_score: Optional[float] = None
ext_data: Optional[Dict[str, Any]] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典(写入 Redis 的 JSON 格式)"""
return {
"alarm_id": self.alarm_id,
"alarm_type": self.alarm_type,
"device_id": self.device_id,
"scene_id": self.scene_id,
"event_time": self.event_time,
"alarm_level": self.alarm_level,
"snapshot_local_path": self.snapshot_local_path,
"algorithm_code": self.algorithm_code,
"confidence_score": self.confidence_score,
"ext_data": self.ext_data,
}
def generate_alarm_id(device_id: str) -> str:
"""
生成告警ID
格式: edge_{device_id}_{YYYYMMDDHHmmss}_{6位uuid}
"""
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
uid = uuid.uuid4().hex[:6]
return f"edge_{device_id}_{timestamp}_{uid}"
# Redis 队列 Key 常量
REDIS_KEY_ALARM_PENDING = "local:alarm:pending"
REDIS_KEY_ALARM_RETRY = "local:alarm:retry"
REDIS_KEY_ALARM_DEAD = "local:alarm:dead"
class ResultReporter:
"""结果上报器
将告警写入本地 Redis 队列 AlarmUploadWorker 异步消费上传
report_alarm() 方法使用 LPUSH算法线程零阻塞
"""
def __init__(self):
self._settings = get_settings()
self._redis: Optional[redis.Redis] = None
self._logger = logging.getLogger("result_reporter")
2026-01-29 18:33:12 +08:00
self._performance_stats = {
"alerts_generated": 0,
"alerts_queued": 0,
"queue_failures": 0,
2026-01-29 18:33:12 +08:00
}
# 图片存储(本地保存截图供 worker 读取上传)
self._image_storage = None
self._db_manager = None
self._logger.info("ResultReporter 初始化完成Redis 缓冲模式)")
def initialize(self):
"""初始化 Redis 连接和本地存储"""
# 初始化本地存储(截图保存)
try:
from core.storage_manager import get_image_storage
self._image_storage = get_image_storage()
except Exception as e:
self._logger.warning(f"本地图片存储初始化失败: {e}")
try:
from config.database import get_sqlite_manager
self._db_manager = get_sqlite_manager()
except Exception as e:
self._logger.warning(f"本地数据库初始化失败: {e}")
# 初始化 Redis 连接(使用本地 Redis
redis_cfg = self._settings.local_redis
try:
self._redis = redis.Redis(
host=redis_cfg.host,
port=redis_cfg.port,
db=redis_cfg.db,
password=redis_cfg.password,
decode_responses=True,
socket_connect_timeout=5,
)
self._redis.ping()
self._logger.info(
f"Redis 连接成功: {redis_cfg.host}:{redis_cfg.port}/{redis_cfg.db}"
)
except Exception as e:
self._logger.error(f"Redis 连接失败: {e}")
self._redis = None
def report_alarm(self, alarm_info: AlarmInfo, screenshot: Optional[np.ndarray] = None) -> bool:
2026-01-29 18:33:12 +08:00
"""
上报告警写入 Redis 队列
2026-01-29 18:33:12 +08:00
Args:
alarm_info: 告警信息
screenshot: 抓拍图片numpy 数组
2026-01-29 18:33:12 +08:00
Returns:
是否成功写入队列
2026-01-29 18:33:12 +08:00
"""
self._performance_stats["alerts_generated"] += 1
# 保存截图到本地,获取本地路径
if screenshot is not None and self._image_storage:
try:
local_path = self._image_storage.save_capture(
image=screenshot,
camera_id=alarm_info.device_id,
alert_id=alarm_info.alarm_id,
timestamp=datetime.now(),
)
if local_path:
alarm_info.snapshot_local_path = local_path
except Exception as e:
self._logger.error(f"保存截图失败: {e}")
# 写入 Redis 队列
if self._redis is None:
self._performance_stats["queue_failures"] += 1
self._logger.error("Redis 未连接,无法写入告警队列")
return False
try:
alarm_json = json.dumps(alarm_info.to_dict(), ensure_ascii=False)
self._redis.lpush(REDIS_KEY_ALARM_PENDING, alarm_json)
self._performance_stats["alerts_queued"] += 1
self._logger.info(
f"告警已入队: alarm_id={alarm_info.alarm_id}, "
f"type={alarm_info.alarm_type}, device={alarm_info.device_id}"
)
return True
except Exception as e:
self._performance_stats["queue_failures"] += 1
self._logger.error(f"写入 Redis 队列失败: {e}")
return False
def report_alarm_resolve(self, resolve_data: dict) -> bool:
"""上报告警结束事件(写入 Redis 队列)"""
if self._redis is None:
self._logger.error("Redis 未连接,无法写入 resolve 队列")
return False
try:
resolve_data["_type"] = "resolve" # 标记类型worker 据此分流
resolve_json = json.dumps(resolve_data, ensure_ascii=False)
self._redis.lpush(REDIS_KEY_ALARM_PENDING, resolve_json)
self._logger.info(f"告警结束事件已入队: alarm_id={resolve_data.get('alarm_id')}")
return True
except Exception as e:
self._logger.error(f"写入 resolve 队列失败: {e}")
return False
def get_statistics(self) -> Dict[str, Any]:
"""获取统计信息"""
stats = self._performance_stats.copy()
if self._redis:
try:
stats["pending_count"] = self._redis.llen(REDIS_KEY_ALARM_PENDING)
stats["retry_count"] = self._redis.llen(REDIS_KEY_ALARM_RETRY)
stats["dead_count"] = self._redis.llen(REDIS_KEY_ALARM_DEAD)
except Exception:
pass
stats["redis_connected"] = self._redis is not None
return stats
def close(self):
"""关闭上报器"""
self._logger.info("ResultReporter 资源清理")
if self._image_storage:
try:
self._image_storage.close()
except Exception:
pass
2026-01-29 18:33:12 +08:00
if self._db_manager:
try:
self._db_manager.close()
except Exception:
pass
2026-01-29 18:33:12 +08:00
if self._redis:
try:
self._redis.close()
except Exception:
pass
2026-01-29 18:33:12 +08:00
self._logger.info("ResultReporter 清理完成")
2026-01-29 18:33:12 +08:00
def cleanup(self):
"""清理资源(别名)"""
self.close()