Initial homelab infrastructure
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user