Initial commit: Location Management Flask app
This commit is contained in:
1
app/services/__init__.py
Normal file
1
app/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Services package."""
|
||||
112
app/services/board_service.py
Normal file
112
app/services/board_service.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""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)
|
||||
71
app/services/workflow_engine.py
Normal file
71
app/services/workflow_engine.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Workflow execution engine.
|
||||
|
||||
When a board input fires a webhook, this module evaluates all matching
|
||||
enabled workflows and triggers the configured relay action.
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from app import db, socketio
|
||||
from app.models.workflow import Workflow
|
||||
from app.models.board import Board
|
||||
from app.services import board_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def process_input_event(board_id: int, input_num: int, new_state: bool) -> None:
|
||||
"""Called from the webhook route. Finds and fires matching workflows."""
|
||||
# Determine event type from state
|
||||
event = "press" if new_state else "release"
|
||||
|
||||
workflows = Workflow.query.filter_by(
|
||||
trigger_board_id=board_id,
|
||||
trigger_input=input_num,
|
||||
is_enabled=True,
|
||||
).all()
|
||||
|
||||
for wf in workflows:
|
||||
if wf.trigger_event not in (event, "both"):
|
||||
continue
|
||||
|
||||
target = db.session.get(Board, wf.action_board_id)
|
||||
if target is None:
|
||||
logger.warning("Workflow %d: target board %d not found", wf.id, wf.action_board_id)
|
||||
continue
|
||||
|
||||
success = False
|
||||
new_relay_state = None
|
||||
|
||||
if wf.action_type == "on":
|
||||
success = board_service.set_relay(target, wf.action_relay, True)
|
||||
new_relay_state = True
|
||||
elif wf.action_type == "off":
|
||||
success = board_service.set_relay(target, wf.action_relay, False)
|
||||
new_relay_state = False
|
||||
elif wf.action_type == "toggle":
|
||||
new_relay_state = board_service.toggle_relay(target, wf.action_relay)
|
||||
success = new_relay_state is not None
|
||||
|
||||
if success:
|
||||
wf.last_triggered = datetime.utcnow()
|
||||
# Update cached relay state in DB
|
||||
if new_relay_state is not None:
|
||||
states = target.relay_states
|
||||
states[f"relay_{wf.action_relay}"] = new_relay_state
|
||||
target.relay_states = states
|
||||
|
||||
db.session.commit()
|
||||
logger.info(
|
||||
"Workflow '%s' fired: Board#%d.input%d=%s → Board#%d.relay%d [%s]",
|
||||
wf.name, board_id, input_num, new_state,
|
||||
target.id, wf.action_relay, wf.action_type,
|
||||
)
|
||||
# Push update so dashboard reflects new relay state instantly
|
||||
socketio.emit("board_update", {
|
||||
"board_id": target.id,
|
||||
"relay_states": target.relay_states,
|
||||
"is_online": target.is_online,
|
||||
})
|
||||
else:
|
||||
logger.warning("Workflow '%s' failed to reach board %s", wf.name, target.name)
|
||||
Reference in New Issue
Block a user