|
|
from __future__ import annotations
|
|
|
|
|
|
import asyncio
|
|
|
import json
|
|
|
import logging
|
|
|
import websockets
|
|
|
from typing import Dict, Any
|
|
|
|
|
|
from . import config, state
|
|
|
|
|
|
logger = logging.getLogger("PTZTracker")
|
|
|
|
|
|
|
|
|
def _dispatch_alarm(payload: Dict[str, Any]) -> None:
|
|
|
"""
|
|
|
Кладём событие тревоги в очередь event-loop из любого потока.
|
|
|
"""
|
|
|
if state.alarm_queue is not None and state.MAIN_LOOP is not None:
|
|
|
try:
|
|
|
state.MAIN_LOOP.call_soon_threadsafe(state.alarm_queue.put_nowait, payload)
|
|
|
except Exception:
|
|
|
# не роняем приложение из-за уведомлений
|
|
|
pass
|
|
|
|
|
|
|
|
|
async def alarm_sender() -> None:
|
|
|
"""
|
|
|
Фоновая задача: отправляет тревоги по WebSocket.
|
|
|
Требование: отправлять *только* минимальный JSON:
|
|
|
{"type":"freq_alarm","data":true|false}
|
|
|
"""
|
|
|
assert state.alarm_queue is not None
|
|
|
backoff = 1.0
|
|
|
|
|
|
while True:
|
|
|
try:
|
|
|
logger.info("[ALARM] Connecting to %s ...", config.ALARM_WS_URL)
|
|
|
async with websockets.connect(
|
|
|
config.ALARM_WS_URL,
|
|
|
ping_interval=20,
|
|
|
close_timeout=1.0,
|
|
|
compression=None,
|
|
|
) as ws:
|
|
|
logger.info("[ALARM] Connected")
|
|
|
backoff = 1.0
|
|
|
|
|
|
while True:
|
|
|
payload = await state.alarm_queue.get()
|
|
|
try:
|
|
|
# Строго минимальный словарь только с нужными ключами
|
|
|
if isinstance(payload, dict):
|
|
|
minimal = {
|
|
|
"type": payload.get("type"),
|
|
|
"data": bool(payload.get("data")),
|
|
|
}
|
|
|
else:
|
|
|
# на случай если кто-то положил уже готовую строку/другую структуру
|
|
|
minimal = payload
|
|
|
|
|
|
# Без лишних пробелов/форматирования
|
|
|
await ws.send(json.dumps(minimal, separators=(",", ":")))
|
|
|
except Exception as exc:
|
|
|
logger.error("[ALARM] Send failed: %s", exc)
|
|
|
try:
|
|
|
# возвращаем событие в очередь, чтобы не потерять
|
|
|
state.alarm_queue.put_nowait(payload)
|
|
|
except Exception:
|
|
|
pass
|
|
|
break
|
|
|
finally:
|
|
|
state.alarm_queue.task_done()
|
|
|
|
|
|
except Exception as exc:
|
|
|
logger.error("[ALARM] Connection error: %s", exc)
|
|
|
|
|
|
await asyncio.sleep(backoff)
|
|
|
backoff = min(backoff * 2, 10.0)
|
|
|
|
|
|
|
|
|
def queue_alarm(cam_idx: int, state_on: bool = True) -> None:
|
|
|
"""
|
|
|
Поставить тревогу в очередь для WS-отправки.
|
|
|
|
|
|
ВАЖНО: WS-полезная нагрузка должна быть строго минимальной,
|
|
|
без дополнительных полей вроде IdCamera/mac.
|
|
|
"""
|
|
|
_dispatch_alarm({"type": "freq_alarm", "data": bool(state_on)})
|
|
|
|
|
|
|
|
|
def queue_alarm_clear(cam_idx: int) -> None:
|
|
|
"""
|
|
|
Снять тревогу (data=false).
|
|
|
"""
|
|
|
queue_alarm(cam_idx, False)
|
|
|
|
|
|
|
|
|
def _send_record_event(cam_idx: int, flag: bool) -> None:
|
|
|
"""
|
|
|
Вспомогательное событие детекции (если используется где-то ещё в коде).
|
|
|
Оставлено без изменений: это не WS-Alarm, а внутренняя очередь.
|
|
|
"""
|
|
|
if state.detection_queue is not None and state.MAIN_LOOP is not None:
|
|
|
try:
|
|
|
state.MAIN_LOOP.call_soon_threadsafe(
|
|
|
state.detection_queue.put_nowait,
|
|
|
{
|
|
|
"type": "detection",
|
|
|
"data": {"detection": bool(flag), "IdCamera": int(cam_idx)},
|
|
|
},
|
|
|
)
|
|
|
except Exception:
|
|
|
pass
|