208 lines
6.8 KiB
Python
208 lines
6.8 KiB
Python
import json
|
|
import sys
|
|
import threading
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
# Add vendor/ to the import path so pip --target dependencies are found.
|
|
vendor_path = Path(__file__).parent / "vendor"
|
|
if vendor_path.exists():
|
|
sys.path.insert(0, str(vendor_path))
|
|
|
|
import requests
|
|
from flask import Flask, jsonify, request, render_template
|
|
from waitress import serve
|
|
|
|
import config
|
|
import db
|
|
|
|
app = Flask(__name__, static_folder="static", template_folder="templates")
|
|
db.init_db()
|
|
|
|
_poller_thread = None
|
|
_stop_polling = threading.Event()
|
|
|
|
|
|
def fetch_cyd():
|
|
try:
|
|
resp = requests.get(
|
|
f"{config.CYD_BASE_URL}/api/latest",
|
|
timeout=10,
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
except Exception as e:
|
|
app.logger.warning(f"CYD fetch failed: {e}")
|
|
return None
|
|
|
|
|
|
def poller_loop():
|
|
while not _stop_polling.is_set():
|
|
session = db.get_active_session()
|
|
if session:
|
|
payload = fetch_cyd()
|
|
if payload:
|
|
db.insert_reading(session["id"], payload)
|
|
time.sleep(config.POLL_INTERVAL)
|
|
|
|
|
|
def start_poller():
|
|
global _poller_thread
|
|
if _poller_thread is None or not _poller_thread.is_alive():
|
|
_stop_polling.clear()
|
|
_poller_thread = threading.Thread(target=poller_loop, daemon=True)
|
|
_poller_thread.start()
|
|
|
|
|
|
@app.route("/")
|
|
def index():
|
|
return render_template("index.html")
|
|
|
|
|
|
@app.route("/api/status")
|
|
def api_status():
|
|
"""Backend status plus current CYD reading."""
|
|
session = db.get_active_session()
|
|
latest = db.get_latest_reading(session["id"]) if session else None
|
|
latest_proxy = fetch_cyd()
|
|
return jsonify({
|
|
"ok": True,
|
|
"cyd_base_url": config.CYD_BASE_URL,
|
|
"poll_interval": config.POLL_INTERVAL,
|
|
"active_session": session,
|
|
"latest_reading": latest_proxy or latest,
|
|
})
|
|
|
|
|
|
@app.route("/api/cyd/latest")
|
|
def api_cyd_latest():
|
|
"""Proxy to the CYD's own /api/latest."""
|
|
payload = fetch_cyd()
|
|
if payload is None:
|
|
return jsonify({"ok": False, "error": "Unable to reach CYD"}), 502
|
|
return jsonify(payload)
|
|
|
|
|
|
def _fetch_cyd_settings():
|
|
try:
|
|
resp = requests.get(f"{config.CYD_BASE_URL}/api/settings", timeout=10)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
except Exception as e:
|
|
app.logger.warning(f"CYD settings fetch failed: {e}")
|
|
return None
|
|
|
|
|
|
@app.route("/api/probes", methods=["GET", "PUT"])
|
|
def probes():
|
|
if request.method == "GET":
|
|
settings = _fetch_cyd_settings()
|
|
if settings is None:
|
|
# Fall back to the latest reading so the UI still has something to show.
|
|
latest = fetch_cyd()
|
|
names = []
|
|
if latest:
|
|
names = [p.get("name", f"Probe {i + 1}") for i, p in enumerate(latest.get("probes", []))]
|
|
if not names:
|
|
names = [f"Probe {i + 1}" for i in range(4)]
|
|
return jsonify({"ok": True, "names": names, "source": "fallback"}), 200
|
|
names = settings.get("probeNames", [f"Probe {i + 1}" for i in range(4)])
|
|
return jsonify({"ok": True, "names": names, "source": "cyd"})
|
|
|
|
# PUT — forward to CYD's /api/settings with just the probeNames field.
|
|
data = request.get_json() or {}
|
|
names = data.get("names", [])
|
|
if not isinstance(names, list) or len(names) != 4 or not all(isinstance(n, str) for n in names):
|
|
return jsonify({"ok": False, "error": "names must be an array of 4 strings"}), 422
|
|
try:
|
|
resp = requests.post(
|
|
f"{config.CYD_BASE_URL}/api/settings",
|
|
json={"probeNames": names},
|
|
timeout=10,
|
|
)
|
|
resp.raise_for_status()
|
|
except Exception as e:
|
|
app.logger.warning(f"CYD settings update failed: {e}")
|
|
return jsonify({"ok": False, "error": f"Unable to update CYD: {e}"}), 502
|
|
|
|
return jsonify({"ok": True, "names": names})
|
|
|
|
|
|
@app.route("/api/sessions", methods=["GET", "POST"])
|
|
def sessions():
|
|
if request.method == "POST":
|
|
data = request.get_json() or {}
|
|
name = data.get("name", f"Cook {datetime.now(timezone.utc).isoformat()[:19]}")
|
|
notes = data.get("notes", "")
|
|
# End any currently active session first.
|
|
active = db.get_active_session()
|
|
if active:
|
|
db.end_session(active["id"])
|
|
session_id = db.create_session(name, notes)
|
|
return jsonify({"ok": True, "session_id": session_id}), 201
|
|
|
|
with db.get_connection() as conn:
|
|
rows = conn.execute(
|
|
"SELECT * FROM sessions ORDER BY started_at DESC"
|
|
).fetchall()
|
|
return jsonify([dict(row) for row in rows])
|
|
|
|
|
|
@app.route("/api/sessions/<int:session_id>", methods=["GET", "PUT", "DELETE"])
|
|
def session_detail(session_id):
|
|
if request.method == "GET":
|
|
with db.get_connection() as conn:
|
|
row = conn.execute(
|
|
"SELECT * FROM sessions WHERE id = ?", (session_id,)
|
|
).fetchone()
|
|
if not row:
|
|
return jsonify({"ok": False, "error": "Session not found"}), 404
|
|
return jsonify(dict(row))
|
|
|
|
if request.method == "PUT":
|
|
data = request.get_json() or {}
|
|
if "notes" in data:
|
|
db.update_session_notes(session_id, data["notes"])
|
|
if data.get("end"):
|
|
db.end_session(session_id)
|
|
return jsonify({"ok": True})
|
|
|
|
if request.method == "DELETE":
|
|
with db.get_connection() as conn:
|
|
conn.execute("DELETE FROM readings WHERE session_id = ?", (session_id,))
|
|
conn.execute("DELETE FROM setpoints WHERE session_id = ?", (session_id,))
|
|
conn.execute("DELETE FROM sessions WHERE id = ?", (session_id,))
|
|
conn.commit()
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
@app.route("/api/readings/<int:session_id>")
|
|
def readings(session_id):
|
|
minutes = request.args.get("minutes", config.DEFAULT_CHART_MINUTES, type=int)
|
|
probe = request.args.get("probe", type=int)
|
|
data = db.get_readings(session_id, minutes=minutes, probe_number=probe)
|
|
return jsonify(data)
|
|
|
|
|
|
@app.route("/api/setpoints/<int:session_id>", methods=["GET", "POST"])
|
|
def setpoints(session_id):
|
|
if request.method == "POST":
|
|
data = request.get_json() or {}
|
|
probe_number = data.get("probe_number")
|
|
label = data.get("label", "")
|
|
target_temp = data.get("target_temp")
|
|
if probe_number is None or target_temp is None:
|
|
return jsonify({"ok": False, "error": "probe_number and target_temp required"}), 422
|
|
db.set_setpoint(session_id, probe_number, label, float(target_temp))
|
|
return jsonify({"ok": True})
|
|
|
|
return jsonify(db.get_setpoints(session_id))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
start_poller()
|
|
print(f"Starting server on http://0.0.0.0:5000")
|
|
print(f"Polling CYD at {config.CYD_BASE_URL} every {config.POLL_INTERVAL}s")
|
|
serve(app, host="0.0.0.0", port=5000)
|