- 新增 alarm_upload_worker.py 异步告警上报(COS+HTTP) - result_reporter 重构为Redis队列模式 - config_sync 适配WVP直推的聚合配置格式 - settings 默认 EDGE_DEVICE_ID 改为 edge - 日志设置非告警模块为WARNING级别减少噪音 - main.py 集成新的告警上报流程 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
220 lines
6.8 KiB
Python
220 lines
6.8 KiB
Python
"""
|
||
结果上报模块
|
||
使用本地 Redis 缓冲 + HTTP 上报替代 MQTT
|
||
|
||
告警流程:
|
||
算法产出告警 → report_alarm() LPUSH 到 Redis → AlarmUploadWorker 异步消费
|
||
"""
|
||
|
||
import json
|
||
import logging
|
||
import uuid
|
||
from datetime import datetime, timezone
|
||
from typing import Any, Dict, List, Optional
|
||
from dataclasses import dataclass, field
|
||
|
||
import numpy as np
|
||
import redis
|
||
|
||
from config.settings import get_settings
|
||
|
||
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")
|
||
|
||
self._performance_stats = {
|
||
"alerts_generated": 0,
|
||
"alerts_queued": 0,
|
||
"queue_failures": 0,
|
||
}
|
||
|
||
# 图片存储(本地保存截图供 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:
|
||
"""
|
||
上报告警(写入 Redis 队列)
|
||
|
||
Args:
|
||
alarm_info: 告警信息
|
||
screenshot: 抓拍图片(numpy 数组)
|
||
|
||
Returns:
|
||
是否成功写入队列
|
||
"""
|
||
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 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
|
||
|
||
if self._db_manager:
|
||
try:
|
||
self._db_manager.close()
|
||
except Exception:
|
||
pass
|
||
|
||
if self._redis:
|
||
try:
|
||
self._redis.close()
|
||
except Exception:
|
||
pass
|
||
|
||
self._logger.info("ResultReporter 清理完成")
|
||
|
||
def cleanup(self):
|
||
"""清理资源(别名)"""
|
||
self.close()
|