from flask import Flask, render_template, jsonify, request, Response import requests import os from functools import wraps from requests.auth import HTTPBasicAuth app = Flask(__name__) WOL_API_URL = os.getenv("WOL_API_URL", "https://wol.zea.lt") WOL_API_USER = os.getenv("WOL_API_USER") WOL_API_PASS = os.getenv("WOL_API_PASS") WEATHER_API_KEY = os.getenv("WEATHER_API_KEY") WEATHER_CITY = os.getenv("WEATHER_CITY", "") def _fetch_wol_status(): """Fetch device list and status from WOL API (single source of truth).""" try: auth = ( HTTPBasicAuth(WOL_API_USER, WOL_API_PASS) if WOL_API_USER and WOL_API_PASS else None ) r = requests.get( f"{WOL_API_URL}/status", auth=auth, timeout=5, ) if r.ok: return r.json().get("devices", {}) except requests.RequestException: pass return {} def check_auth(username, password): return username == WOL_API_USER and password == WOL_API_PASS def authenticate(): """Sends a 401 response that enables basic auth.""" return Response( "Could not verify your access level for that URL.\n" "You have to login with proper credentials", 401, {"WWW-Authenticate": 'Basic realm="Login Required"'}, ) def requires_auth(f): @wraps(f) def decorated(*args, **kwargs): auth = request.authorization if not auth or not check_auth(auth.username, auth.password): return authenticate() return f(*args, **kwargs) return decorated @app.route("/") def dashboard(): devices_data = _fetch_wol_status() devices = {k: v.get("mac", "") for k, v in devices_data.items()} status = {k: v.get("online", False) for k, v in devices_data.items()} return render_template( "dashboard.html", devices=devices, status=status, ) @app.route("/wake/", methods=["POST"]) @requires_auth def wake_device(device): devices_data = _fetch_wol_status() if device not in devices_data: return jsonify({"error": "Device not found"}), 404 try: auth = HTTPBasicAuth(WOL_API_USER, WOL_API_PASS) r = requests.post( f"{WOL_API_URL}/wake/{device}", auth=auth, timeout=5, ) if r.ok: return jsonify({"message": f"Wake command sent to {device}"}) return jsonify({"error": f"Failed to send wake command: {r.text}"}), r.status_code except requests.RequestException as e: return jsonify({"error": str(e)}), 500 @app.route("/weather") def weather(): """Proxy for OpenWeatherMap – keeps API key server-side.""" if not WEATHER_API_KEY or not WEATHER_CITY: return jsonify({"error": "Weather not configured"}), 503 try: r = requests.get( "https://api.openweathermap.org/data/2.5/weather", params={ "q": WEATHER_CITY, "appid": WEATHER_API_KEY, "units": "metric", }, timeout=5, ) if r.ok: return jsonify(r.json()) except requests.RequestException: pass return jsonify({"error": "Weather unavailable"}), 503 if __name__ == "__main__": app.run(host="0.0.0.0", port=8080)