"""Command dispatch — между MQTT/HTTP командами и ZMQ выходом. Action kinds: layout.set — set_layout (future Phase 4+: auto_mode.set, focus_camera.set, overlay.add, ...) """ from __future__ import annotations import structlog from .config import Config, InstanceCfg from .layouts import PREDEFINED_LAYOUTS from .state import ControllerState from .zmq_client import FFmpegZmqClient log = structlog.get_logger() class CommandDispatcher: def __init__(self, cfg: Config, state: ControllerState) -> None: self.cfg = cfg self.state = state self._zmq_clients: dict[str, FFmpegZmqClient] = {} # publish callback устанавливается из вне (MqttLoop) self.on_state_change = None # type: ignore[var-annotated] self.on_event = None # type: ignore[var-annotated] def _client(self, inst: InstanceCfg) -> FFmpegZmqClient: c = self._zmq_clients.get(inst.name) if c is None: c = FFmpegZmqClient(inst.zmq_endpoint) self._zmq_clients[inst.name] = c return c def _find_instance(self, name: str) -> InstanceCfg | None: return next((i for i in self.cfg.instances if i.name == name), None) async def handle(self, instance_name: str, kind: str, payload: str) -> None: inst = self._find_instance(instance_name) if inst is None: log.warning("dispatch.unknown_instance", instance=instance_name) return if kind == "layout.set": await self._set_layout(inst, payload.strip()) else: log.warning("dispatch.unknown_kind", instance=instance_name, kind=kind) async def _set_layout(self, inst: InstanceCfg, layout: str) -> None: if layout not in PREDEFINED_LAYOUTS: log.warning( "dispatch.unknown_layout", instance=inst.name, layout=layout, available=PREDEFINED_LAYOUTS, ) return old = await self.state.get_layout(inst.name) client = self._client(inst) try: reply = await client.send_command( inst.filter_target, "layout", layout ) log.info( "dispatch.layout_set", instance=inst.name, layout=layout, ffmpeg_reply=reply, ) except (TimeoutError, Exception) as e: log.error("dispatch.zmq_fail", instance=inst.name, error=str(e)) return await self.state.set_layout(inst.name, layout) if self.on_state_change: await self.on_state_change(inst.name, "layout", layout) if self.on_event: await self.on_event( inst.name, "layout_switched", {"from": old, "to": layout, "reason": "mqtt"}, ) async def close(self) -> None: for c in self._zmq_clients.values(): await c.close()