Files
homelab/dashboard/app.py
T
2026-02-13 19:41:46 +02:00

117 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/<device>", 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)