"""Layout management routes.""" import json from datetime import datetime from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, abort from flask_login import login_required, current_user from app import db from app.models.layout import Layout from app.models.board import Board from app.models.sonoff_device import SonoffDevice from app.models.tuya_device import TuyaDevice layouts_bp = Blueprint("layouts", __name__) # ── list ────────────────────────────────────────────────────────────────────── @layouts_bp.route("/") @login_required def list_layouts(): layouts = Layout.query.order_by(Layout.name).all() return render_template("layouts/list.html", layouts=layouts) # ── create ──────────────────────────────────────────────────────────────────── @layouts_bp.route("/create", methods=["POST"]) @login_required def create_layout(): if not current_user.is_admin(): abort(403) name = request.form.get("name", "").strip() description = request.form.get("description", "").strip() if not name: flash("Layout name is required.", "danger") return redirect(url_for("layouts.list_layouts")) layout = Layout(name=name, description=description or None) db.session.add(layout) db.session.commit() return redirect(url_for("layouts.builder", layout_id=layout.id)) # ── builder ─────────────────────────────────────────────────────────────────── @layouts_bp.route("//builder") @login_required def builder(layout_id: int): layout = db.get_or_404(Layout, layout_id) boards = Board.query.order_by(Board.name).all() # Build a boards palette for the JS device sidebar boards_data = [] for b in boards: bd = {"id": b.id, "name": b.name, "online": b.is_online, "relays": [], "inputs": [], "sonoff_channels": [], "tuya_channels": [], "board_type": b.board_type} # ── Standard relay/input boards ────────────────────────────────────── for n in range(1, b.num_relays + 1): e = b.get_relay_entity(n) bd["relays"].append({ "num": n, "name": e["name"], "icon": e["icon"], "onColor": e["on_color"], "offColor": e["off_color"], "isOn": b.relay_states.get(f"relay_{n}", False), }) for n in range(1, b.num_inputs + 1): e = b.get_input_entity(n) bd["inputs"].append({ "num": n, "name": e["name"], "icon": e["icon"], "activeColor": e["active_color"], "idleColor": e["idle_color"], "rawState": b.input_states.get(f"input_{n}", True), }) # ── Sonoff eWeLink sub-devices ──────────────────────────────────────── if b.board_type == "sonoff_ewelink": KIND_ICON = { "switch": "bi-toggles", "light": "bi-lightbulb-fill", "fan": "bi-fan", "sensor": "bi-thermometer-half", "remote": "bi-broadcast", } devices = SonoffDevice.query.filter_by(board_id=b.id).order_by( SonoffDevice.name).all() for dev in devices: icon = KIND_ICON.get(dev.kind, "bi-toggles") num_ch = max(dev.num_channels, 1) for ch in range(num_ch): label = dev.name if num_ch == 1 else f"{dev.name} – Ch{ch + 1}" bd["sonoff_channels"].append({ "deviceId": dev.device_id, "deviceDbId": dev.id, "channel": ch, "name": label, "icon": icon, "kind": dev.kind, "isOn": dev.get_channel_state(ch), "isOnline": dev.is_online, }) # ── Tuya Cloud sub-devices ──────────────────────────────────────────── if b.board_type == "tuya_cloud": TUYA_KIND_ICON = { "switch": "bi-toggles", "light": "bi-lightbulb-fill", "fan": "bi-fan", "sensor": "bi-thermometer-half", "cover": "bi-door-open", } tuya_devs = TuyaDevice.query.filter_by(board_id=b.id).order_by( TuyaDevice.name).all() for dev in tuya_devs: icon = TUYA_KIND_ICON.get(dev.kind, "bi-plug") for dp in dev.switch_dps: idx = dev.switch_dps.index(dp) label = (dev.name if dev.num_channels == 1 else f"{dev.name} – Ch{idx + 1}") bd["tuya_channels"].append({ "deviceId": dev.device_id, "channel": dp, # dp_code string, e.g. "switch_1" "name": label, "icon": icon, "kind": dev.kind, "isOn": dev.status.get(dp, False), "isOnline": dev.is_online, }) boards_data.append(bd) return render_template( "layouts/builder.html", layout=layout, boards_data=boards_data, ) # ── save (AJAX) ─────────────────────────────────────────────────────────────── @layouts_bp.route("//save", methods=["POST"]) @login_required def save_layout(layout_id: int): layout = db.get_or_404(Layout, layout_id) data = request.get_json(silent=True) or {} if "canvas" in data: layout.canvas_json = json.dumps(data["canvas"]) if data.get("thumbnail"): layout.thumbnail_b64 = data["thumbnail"] layout.updated_at = datetime.utcnow() db.session.commit() return jsonify({"status": "ok"}) # ── delete ──────────────────────────────────────────────────────────────────── @layouts_bp.route("//delete", methods=["POST"]) @login_required def delete_layout(layout_id: int): if not current_user.is_admin(): abort(403) layout = db.get_or_404(Layout, layout_id) db.session.delete(layout) db.session.commit() flash(f"Layout '{layout.name}' deleted.", "warning") return redirect(url_for("layouts.list_layouts"))