Files
vf-cuda-grid/controller/cuda_grid_controller/frigate_bridge.py
T
gx a1090a5f4c controller: Phase 4a — overlay infrastructure (data models + API + Frigate bridge skeleton)
Phase 4a deliverable (no filter rendering yet — это Phase 4b).
End-to-end pipeline: HA/HTTP/MQTT → controller → ZMQ → FFmpeg (logged).

Modules:
- overlays.py — 7 discriminated union types через pydantic:
  rect, text, icon, image, dim, graph, chat. Normalized coords (0.0-1.0),
  optional cell binding, z_order, opacity, visible.
- state.py — overlay storage per instance (CRUD: add/remove/update/get/clear)
- dispatch.py — overlay.add/remove/clear actions:
  - parses JSON payload в Overlay через TypeAdapter
  - serializes to ZMQ string: "<id> <type> <full-json>"
  - sends via FFmpeg process_command (filter will парсить в Phase 4b)
  - updates state + publishes events (overlay_added, overlay_removed, overlays_cleared)
- http_api.py — REST endpoints:
  - POST /overlay/{inst}/add (body = Overlay JSON, returns id)
  - GET /overlay/{inst} — list all
  - DELETE /overlay/{inst}/{id} — single
  - DELETE /overlay/{inst} — clear all
  - PATCH /overlay/{inst}/{id} — update
- mqtt_loop.py — already subscribes cuda_grid/cmd/<inst>/+/+; teper handles
  overlay/add (JSON payload), overlay/remove (id), overlay/clear
- frigate_bridge.py — FrigateBridge skeleton:
  - subscribe frigate/+/motion + frigate/events
  - mapping camera_name → target_instance + cell index
  - Phase 4a: log received events (rendering в Phase 4b)
- config.py — frigate: optional section
- examples/controller.yaml — frigate mappings для 4 наших камер

State management:
- ControllerState.add/remove/update/get/clear_overlay (asyncio.Lock guarded)
- InstanceState.overlays: dict[str, Overlay]
- IDs generated via uuid4()[:8]

Phase 4a limitations:
- Filter side ничего не рендерит (just logs ZMQ commands)
- Frigate bridge принимает events но не auto-generates overlays
- HA Discovery не имеет overlay-specific entities (overlays через REST API)

Phase 4b: filter-side AVFrame side data + CUDA kernels (rect first, NPP-based,
потом text via freetype atlas, потом icon sprite blit).
2026-05-19 22:03:20 +01:00

99 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Frigate MQTT bridge — subscribe `frigate/+/motion` + `frigate/events`,
маппит к overlay add/remove на configured instance.
Phase 4a: skeleton — receives + logs events, не делает auto-overlay yet.
Phase 4b: actual auto-bbox overlays для detect events.
"""
from __future__ import annotations
import json
import structlog
from pydantic import BaseModel, Field
log = structlog.get_logger()
class FrigateCameraMapping(BaseModel):
"""Mapping Frigate camera_name → vf-cuda-grid cell index в instance."""
frigate_camera: str
target_instance: str
cell: int = Field(default=0, description="Cell index в layout куда рисовать bbox")
enabled: bool = True
class FrigateBridgeCfg(BaseModel):
enabled: bool = False
base_topic: str = "frigate"
mappings: list[FrigateCameraMapping] = []
class FrigateBridge:
"""Подписывается на frigate/* topics; transforms к overlay commands.
Phase 4a: just log events. Phase 4b will:
- на frigate/<cam>/motion ON → add dim overlay на cell (highlight motion)
- на frigate/events object_detected → add rect+text overlay с label+confidence
- на event ended → remove overlay
"""
def __init__(self, cfg: FrigateBridgeCfg) -> None:
self.cfg = cfg
# cam_name → mapping (для быстрого lookup)
self._by_camera = {m.frigate_camera: m for m in cfg.mappings if m.enabled}
def topics_to_subscribe(self) -> list[str]:
if not self.cfg.enabled:
return []
base = self.cfg.base_topic.rstrip("/")
return [f"{base}/+/motion", f"{base}/events"]
def handle_message(self, topic: str, payload: str) -> None:
"""Logging + future overlay generation."""
if not self.cfg.enabled:
return
base = self.cfg.base_topic.rstrip("/")
# frigate/<cam>/motion
if topic.startswith(f"{base}/") and topic.endswith("/motion"):
cam = topic[len(base) + 1 : -len("/motion")]
mapping = self._by_camera.get(cam)
if mapping is None:
return
state = payload.strip().upper()
log.info(
"frigate.motion",
camera=cam,
state=state,
target=mapping.target_instance,
cell=mapping.cell,
)
# Phase 4b: добавлять/удалять dim overlay в зависимости от state
return
# frigate/events — JSON с details
if topic == f"{base}/events":
try:
event = json.loads(payload)
cam = event.get("after", {}).get("camera") or event.get("before", {}).get("camera")
event_type = event.get("type", "?")
label = event.get("after", {}).get("label", "?")
mapping = self._by_camera.get(cam) if cam else None
if mapping is None:
return
log.info(
"frigate.event",
camera=cam,
type=event_type,
label=label,
target=mapping.target_instance,
cell=mapping.cell,
)
# Phase 4b: для event_type=new → add rect+text overlay;
# для event_type=end → remove overlay
except json.JSONDecodeError as e:
log.warning("frigate.event_parse_fail", error=str(e))