"""Board service — thin dispatcher layer. All board-type-specific logic lives in ``app/drivers//driver.py``. This module just resolves the right driver from the registry and calls it. """ from __future__ import annotations import logging import threading from datetime import datetime from app import db, socketio from app.models.board import Board from app.drivers.registry import registry logger = logging.getLogger(__name__) def _driver(board: Board): """Resolve the driver for a board, falling back gracefully.""" drv = registry.get(board.board_type) if drv is None: logger.warning( "No driver found for board_type='%s' (board '%s'). " "Is the driver folder present in app/drivers/?", board.board_type, board.name, ) return drv # ── relay control (public API used by routes) ───────────────────────────────── def get_relay_status(board: Board, relay_num: int) -> bool | None: drv = _driver(board) return drv.get_relay_status(board, relay_num) if drv else None def set_relay(board: Board, relay_num: int, state: bool) -> bool: drv = _driver(board) return drv.set_relay(board, relay_num, state) if drv else False def toggle_relay(board: Board, relay_num: int) -> bool | None: drv = _driver(board) return drv.toggle_relay(board, relay_num) if drv else None # ── polling ─────────────────────────────────────────────────────────────────── def poll_board(app, board_id: int) -> None: """Fetch all I/O states for one board and persist to DB.""" with app.app_context(): board = db.session.get(Board, board_id) if board is None: return drv = _driver(board) if drv is None: return result = drv.poll(board) # Inputs: hardware is source of truth — always overwrite from poll if result.get("input_states"): board.input_states = result["input_states"] # Relays: server is source of truth — only sync from hardware when the # board was previously offline (re-sync after reconnect), not during # normal operation (to avoid overwriting optimistic state from commands) was_offline = not board.is_online if was_offline and result.get("relay_states"): board.relay_states = result["relay_states"] board.is_online = result.get("is_online", False) if result.get("firmware_version"): board.firmware_version = result["firmware_version"] if board.is_online: board.last_seen = datetime.utcnow() db.session.commit() socketio.emit("board_update", { "board_id": board.id, "is_online": board.is_online, "relay_states": board.relay_states, "input_states": board.input_states, }) def poll_all_boards(app) -> None: """Poll every registered board in parallel.""" with app.app_context(): board_ids = [r[0] for r in db.session.query(Board.id).all()] threads = [ threading.Thread(target=poll_board, args=(app, bid), daemon=True) for bid in board_ids ] for t in threads: t.start() for t in threads: t.join(timeout=4) # ── webhook registration ────────────────────────────────────────────────────── def register_webhook(board: Board, server_base_url: str) -> bool: drv = _driver(board) if drv is None: return False callback_url = f"{server_base_url}/api/webhook/{board.id}" return drv.register_webhook(board, callback_url)