"""Board management routes.""" import json from flask import Blueprint, render_template, redirect, url_for, flash, request, abort, jsonify from flask_login import login_required, current_user from app import db from app.models.board import Board from app.services.board_service import poll_board, register_webhook, set_relay from app.drivers.registry import registry from flask import current_app boards_bp = Blueprint("boards", __name__) def _board_types(): """Dynamic list read from the driver registry — auto-updates when drivers are added.""" return registry.choices() # ── list ────────────────────────────────────────────────────────────────────── @boards_bp.route("/") @login_required def list_boards(): boards = Board.query.order_by(Board.name).all() return render_template("boards/list.html", boards=boards) # ── detail / quick controls ─────────────────────────────────────────────────── @boards_bp.route("/") @login_required def board_detail(board_id: int): board = db.get_or_404(Board, board_id) # Sonoff eWeLink gateway boards have their own page if board.board_type == "sonoff_ewelink": return redirect(url_for("sonoff.gateway", board_id=board_id)) # Refresh states from device poll_board(current_app._get_current_object(), board_id) board = db.session.get(Board, board_id) return render_template("boards/detail.html", board=board) # ── add ─────────────────────────────────────────────────────────────────────── @boards_bp.route("/add", methods=["GET", "POST"]) @login_required def add_board(): if not current_user.is_admin(): abort(403) if request.method == "POST": name = request.form.get("name", "").strip() board_type = request.form.get("board_type", "olimex_esp32_c6_evb") host = request.form.get("host", "").strip() port = int(request.form.get("port", 80)) num_relays = int(request.form.get("num_relays", 4)) num_inputs = int(request.form.get("num_inputs", 4)) # Sonoff gateway doesn't need a real host address is_gateway = board_type == "sonoff_ewelink" if not name or (not host and not is_gateway): flash("Name and host are required.", "danger") return render_template("boards/add.html", board_types=_board_types(), drivers=registry.all()) if is_gateway: host = host or "ewelink.cloud" num_relays = 0 num_inputs = 0 board = Board( name=name, board_type=board_type, host=host, port=port, num_relays=num_relays, num_inputs=num_inputs, ) db.session.add(board) db.session.commit() # Try to register webhook immediately server_url = current_app.config.get("SERVER_BASE_URL", "http://localhost:5000") register_webhook(board, server_url) flash(f"Board '{name}' added successfully.", "success") return redirect(url_for("boards.board_detail", board_id=board.id)) return render_template("boards/add.html", board_types=_board_types(), drivers=registry.all()) # ── edit ────────────────────────────────────────────────────────────────────── @boards_bp.route("//edit", methods=["GET", "POST"]) @login_required def edit_board(board_id: int): if not current_user.is_admin(): abort(403) board = db.get_or_404(Board, board_id) if request.method == "POST": board.name = request.form.get("name", board.name).strip() board.host = request.form.get("host", board.host).strip() board.port = int(request.form.get("port", board.port)) board.board_type = request.form.get("board_type", board.board_type) board.num_relays = int(request.form.get("num_relays", board.num_relays)) board.num_inputs = int(request.form.get("num_inputs", board.num_inputs)) # Update labels labels = {} for n in range(1, board.num_relays + 1): lbl = request.form.get(f"relay_{n}_label", "").strip() if lbl: labels[f"relay_{n}"] = lbl for n in range(1, board.num_inputs + 1): lbl = request.form.get(f"input_{n}_label", "").strip() if lbl: labels[f"input_{n}"] = lbl board.labels = labels db.session.commit() flash("Board updated.", "success") return redirect(url_for("boards.board_detail", board_id=board.id)) return render_template("boards/edit.html", board=board, board_types=_board_types()) # ── delete ──────────────────────────────────────────────────────────────────── @boards_bp.route("//delete", methods=["POST"]) @login_required def delete_board(board_id: int): if not current_user.is_admin(): abort(403) board = db.get_or_404(Board, board_id) db.session.delete(board) db.session.commit() flash(f"Board '{board.name}' deleted.", "warning") return redirect(url_for("boards.list_boards")) # ── quick relay toggle ─────────────────────────────────────────────────────── @boards_bp.route("//relay//toggle", methods=["POST"]) @login_required def toggle_relay_view(board_id: int, relay_num: int): from app import socketio board = db.get_or_404(Board, board_id) # Always flip the local cached state first (optimistic update) states = board.relay_states current = states.get(f"relay_{relay_num}", False) new_state = not current states[f"relay_{relay_num}"] = new_state board.relay_states = states db.session.commit() # Best-effort: send the command to the physical board using set_relay # (uses /relay/on or /relay/off — same endpoints as the detail page ON/OFF buttons) hw_ok = set_relay(board, relay_num, new_state) # Push live update to all clients socketio.emit("board_update", { "board_id": board_id, "is_online": board.is_online, "relay_states": board.relay_states, "input_states": board.input_states, }) label = board.get_relay_label(relay_num) status_text = "ON" if new_state else "OFF" hw_warning = not hw_ok # True when board was unreachable # JSON response for AJAX callers (dashboard) if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html: return jsonify({ "relay": relay_num, "state": new_state, "label": label, "hw_ok": not hw_warning, }) # HTML response for form-submit callers (detail page) if hw_warning: flash(f"{label}: {status_text} (board unreachable — local state updated)", "warning") else: flash(f"{label}: {status_text}", "info") return redirect(url_for("boards.board_detail", board_id=board_id)) # ── quick relay set ─────────────────────────────────────────────────────────── @boards_bp.route("//relay//set", methods=["POST"]) @login_required def set_relay_view(board_id: int, relay_num: int): from app import socketio board = db.get_or_404(Board, board_id) state = request.form.get("state", "off") == "on" # Always update local state states = board.relay_states states[f"relay_{relay_num}"] = state board.relay_states = states db.session.commit() # Best-effort send to hardware hw_ok = set_relay(board, relay_num, state) socketio.emit("board_update", { "board_id": board_id, "is_online": board.is_online, "relay_states": board.relay_states, "input_states": board.input_states, }) label = board.get_relay_label(relay_num) status_text = "ON" if state else "OFF" if not hw_ok: flash(f"{label}: {status_text} (board unreachable — local state updated)", "warning") else: flash(f"{label}: {status_text}", "info") return redirect(url_for("boards.board_detail", board_id=board_id)) # ── edit entity configuration ───────────────────────────────────────────────── @boards_bp.route("//entities", methods=["GET", "POST"]) @login_required def edit_entities(board_id: int): if not current_user.is_admin(): abort(403) board = db.get_or_404(Board, board_id) if request.method == "POST": entities = {} for n in range(1, board.num_relays + 1): entities[f"relay_{n}"] = { "type": request.form.get(f"relay_{n}_type", "switch"), "name": request.form.get(f"relay_{n}_name", "").strip()[:20], "icon": request.form.get(f"relay_{n}_icon", "").strip(), } for n in range(1, board.num_inputs + 1): entities[f"input_{n}"] = { "type": request.form.get(f"input_{n}_type", "generic"), "name": request.form.get(f"input_{n}_name", "").strip()[:20], "icon": request.form.get(f"input_{n}_icon", "").strip(), } board.entities = entities db.session.commit() flash("Entity configuration saved.", "success") return redirect(url_for("boards.board_detail", board_id=board_id)) from app.models.entity_types import RELAY_ENTITY_TYPES, INPUT_ENTITY_TYPES, ICON_PALETTE return render_template( "boards/edit_entities.html", board=board, relay_types=RELAY_ENTITY_TYPES, input_types=INPUT_ENTITY_TYPES, icon_palette=ICON_PALETTE, ) # ── edit labels (legacy — redirects to edit_entities) ───────────────────────── @boards_bp.route("//labels", methods=["GET", "POST"]) @login_required def edit_labels(board_id: int): return redirect(url_for("boards.edit_entities", board_id=board_id))