a1090a5f4c
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).
99 lines
3.5 KiB
Python
99 lines
3.5 KiB
Python
"""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))
|