Files
location_managemet/app/services/board_service.py
2026-02-26 19:24:17 +02:00

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)