"""Sonoff eWeLink Gateway routes. URL structure: GET /sonoff/ – gateway overview GET /sonoff//auth – auth settings form POST /sonoff//auth – save credentials + login POST /sonoff//sync – sync devices from cloud GET /sonoff//device/ – device detail POST /sonoff//device//ch//on|off|toggle – relay control POST /sonoff//device//rename – rename a device """ import json from flask import ( Blueprint, abort, flash, jsonify, redirect, render_template, request, url_for, ) from flask_login import current_user, login_required from app import db, socketio from app.models.board import Board from app.models.sonoff_device import SonoffDevice from app.drivers.registry import registry sonoff_bp = Blueprint("sonoff", __name__, url_prefix="/sonoff") def _get_gateway(board_id: int) -> Board: board = db.get_or_404(Board, board_id) if board.board_type != "sonoff_ewelink": abort(404) return board def _get_driver(board: Board): drv = registry.get("sonoff_ewelink") if drv is None: abort(500) return drv # ── Gateway overview ────────────────────────────────────────────────────────── @sonoff_bp.route("/") @login_required def gateway(board_id: int): board = _get_gateway(board_id) devices = SonoffDevice.query.filter_by(board_id=board_id).order_by( SonoffDevice.name ).all() has_credentials = bool(board.config.get("ewelink_username") and board.config.get("ewelink_password")) return render_template( "sonoff/gateway.html", board=board, devices=devices, has_credentials=has_credentials, ) # ── Auth settings ───────────────────────────────────────────────────────────── @sonoff_bp.route("//auth", methods=["GET", "POST"]) @login_required def auth_settings(board_id: int): if not current_user.is_admin(): abort(403) board = _get_gateway(board_id) if request.method == "POST": username = request.form.get("username", "").strip() password = request.form.get("password", "").strip() country = request.form.get("country_code", "+1").strip() if not username or not password: flash("Username and password are required.", "danger") return redirect(url_for("sonoff.auth_settings", board_id=board_id)) cfg = board.config cfg["ewelink_username"] = username cfg["ewelink_password"] = password cfg["ewelink_country_code"] = country cfg.pop("ewelink_at", None) # force re-login board.config = cfg db.session.commit() # Try to login immediately drv = _get_driver(board) if drv.ensure_token(board): db.session.commit() flash("✓ Connected to eWeLink successfully.", "success") return redirect(url_for("sonoff.gateway", board_id=board_id)) else: flash("Login failed — check credentials.", "danger") return redirect(url_for("sonoff.auth_settings", board_id=board_id)) # Country codes for dropdown from app.drivers.sonoff_ewelink.ewelink_api import REGIONS countries = sorted( [(code, f"{info[0]} ({code})") for code, info in REGIONS.items()], key=lambda x: x[1] ) return render_template( "sonoff/auth_settings.html", board=board, countries=countries, current_country=board.config.get("ewelink_country_code", "+1"), ) # ── Sync devices ────────────────────────────────────────────────────────────── @sonoff_bp.route("//sync", methods=["POST"]) @login_required def sync_devices(board_id: int): if not current_user.is_admin(): abort(403) board = _get_gateway(board_id) drv = _get_driver(board) try: count = drv.sync_devices(board) db.session.commit() flash(f"✓ Synced {count} devices from eWeLink.", "success") except Exception as exc: flash(f"Sync failed: {exc}", "danger") return redirect(url_for("sonoff.gateway", board_id=board_id)) # ── Device detail ───────────────────────────────────────────────────────────── @sonoff_bp.route("//device/") @login_required def device_detail(board_id: int, device_id: str): board = _get_gateway(board_id) dev = SonoffDevice.query.filter_by( board_id=board_id, device_id=device_id ).first_or_404() return render_template("sonoff/device.html", board=board, device=dev) # ── Relay / channel control ─────────────────────────────────────────────────── @sonoff_bp.route( "//device//ch//", methods=["POST"], ) @login_required def control_channel(board_id: int, device_id: str, channel: int, action: str): board = _get_gateway(board_id) dev = SonoffDevice.query.filter_by( board_id=board_id, device_id=device_id ).first_or_404() drv = _get_driver(board) if action == "on": new_state = True elif action == "off": new_state = False elif action == "toggle": new_state = not dev.get_channel_state(channel) else: abort(400) ok = drv.set_device_channel(board, device_id, channel, new_state) # Emit SocketIO update socketio.emit("sonoff_update", { "board_id": board_id, "device_id": device_id, "channel": channel, "state": new_state, "ok": ok, }) if request.accept_mimetypes.best == "application/json" or \ request.headers.get("Accept") == "application/json": return jsonify({ "ok": ok, "state": new_state, "device_id": device_id, "channel": channel, }) if not ok: flash(f"⚠ Command sent but device may not have responded.", "warning") return redirect(url_for("sonoff.gateway", board_id=board_id)) # ── Rename device ───────────────────────────────────────────────────────────── @sonoff_bp.route( "//device//rename", methods=["POST"] ) @login_required def rename_device(board_id: int, device_id: str): if not current_user.is_admin(): abort(403) board = _get_gateway(board_id) dev = SonoffDevice.query.filter_by( board_id=board_id, device_id=device_id ).first_or_404() new_name = request.form.get("name", "").strip()[:64] if new_name: dev.name = new_name db.session.commit() if request.headers.get("Accept") == "application/json": return jsonify({"ok": True, "name": dev.name}) return redirect(url_for("sonoff.device_detail", board_id=board_id, device_id=device_id)) # ── Update LAN IP for a device ──────────────────────────────────────────────── @sonoff_bp.route( "//device//set_ip", methods=["POST"] ) @login_required def set_device_ip(board_id: int, device_id: str): if not current_user.is_admin(): abort(403) board = _get_gateway(board_id) dev = SonoffDevice.query.filter_by( board_id=board_id, device_id=device_id ).first_or_404() ip = request.form.get("ip", "").strip() port = int(request.form.get("port", 8081)) dev.ip_address = ip dev.port = port db.session.commit() flash(f"✓ LAN address updated for {dev.name}.", "success") return redirect(url_for("sonoff.device_detail", board_id=board_id, device_id=device_id))