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