113 lines
3.8 KiB
Python
113 lines
3.8 KiB
Python
"""Board service — thin dispatcher layer.
|
|
|
|
All board-type-specific logic lives in ``app/drivers/<board_type>/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)
|