Add a Flask backend + SQLite + HTML/CSS/JS PWA that: - Polls the CYD /api/latest endpoint - Stores readings, sessions, and freeform notes in SQLite - Displays probe cards with target setpoints and preset options - Renders temperature history with Chart.js - Serves a PWA manifest and dark-themed dashboard Also update cyd-bridge/README.md with verified bringup instructions.
215 lines
6.4 KiB
Python
215 lines
6.4 KiB
Python
import sqlite3
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from datetime import datetime, timezone
|
|
|
|
import config
|
|
|
|
INIT_SQL = """
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
ended_at TEXT,
|
|
notes TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS readings (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id INTEGER NOT NULL,
|
|
received_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
device_id TEXT,
|
|
mac TEXT,
|
|
ble_connected INTEGER,
|
|
battery INTEGER,
|
|
unit TEXT,
|
|
probe1_temp REAL,
|
|
probe1_connected INTEGER,
|
|
probe2_temp REAL,
|
|
probe2_connected INTEGER,
|
|
probe3_temp REAL,
|
|
probe3_connected INTEGER,
|
|
probe4_temp REAL,
|
|
probe4_connected INTEGER,
|
|
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS setpoints (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id INTEGER NOT NULL,
|
|
probe_number INTEGER NOT NULL,
|
|
label TEXT,
|
|
target_temp REAL NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
FOREIGN KEY (session_id) REFERENCES sessions(id),
|
|
UNIQUE(session_id, probe_number)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_readings_session_time
|
|
ON readings(session_id, received_at);
|
|
"""
|
|
|
|
|
|
def init_db():
|
|
Path(config.DATABASE_PATH).parent.mkdir(parents=True, exist_ok=True)
|
|
with get_connection() as conn:
|
|
conn.executescript(INIT_SQL)
|
|
conn.commit()
|
|
|
|
|
|
@contextmanager
|
|
def get_connection():
|
|
conn = sqlite3.connect(config.DATABASE_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
try:
|
|
yield conn
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def utc_now_iso():
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def create_session(name: str, notes: str = ""):
|
|
with get_connection() as conn:
|
|
cursor = conn.execute(
|
|
"INSERT INTO sessions (name, notes) VALUES (?, ?)",
|
|
(name, notes),
|
|
)
|
|
conn.commit()
|
|
return cursor.lastrowid
|
|
|
|
|
|
def get_active_session():
|
|
with get_connection() as conn:
|
|
row = conn.execute(
|
|
"SELECT * FROM sessions WHERE ended_at IS NULL ORDER BY started_at DESC LIMIT 1"
|
|
).fetchone()
|
|
return dict(row) if row else None
|
|
|
|
|
|
def end_session(session_id: int):
|
|
with get_connection() as conn:
|
|
conn.execute(
|
|
"UPDATE sessions SET ended_at = ? WHERE id = ?",
|
|
(utc_now_iso(), session_id),
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def update_session_notes(session_id: int, notes: str):
|
|
with get_connection() as conn:
|
|
conn.execute(
|
|
"UPDATE sessions SET notes = ? WHERE id = ?",
|
|
(notes, session_id),
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def insert_reading(session_id: int, payload: dict):
|
|
probes = payload.get("probes", [])
|
|
temps = []
|
|
connected = []
|
|
for i in range(4):
|
|
if i < len(probes) and probes[i].get("connected"):
|
|
temps.append(probes[i].get("temperature"))
|
|
connected.append(1)
|
|
else:
|
|
temps.append(None)
|
|
connected.append(0)
|
|
|
|
with get_connection() as conn:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO readings (
|
|
session_id, received_at, device_id, mac, ble_connected, battery, unit,
|
|
probe1_temp, probe1_connected,
|
|
probe2_temp, probe2_connected,
|
|
probe3_temp, probe3_connected,
|
|
probe4_temp, probe4_connected
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
session_id,
|
|
utc_now_iso(),
|
|
payload.get("deviceId"),
|
|
payload.get("mac"),
|
|
1 if payload.get("connected") else 0,
|
|
payload.get("battery"),
|
|
payload.get("unit"),
|
|
temps[0], connected[0],
|
|
temps[1], connected[1],
|
|
temps[2], connected[2],
|
|
temps[3], connected[3],
|
|
),
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def get_latest_reading(session_id: int):
|
|
with get_connection() as conn:
|
|
row = conn.execute(
|
|
"SELECT * FROM readings WHERE session_id = ? ORDER BY received_at DESC LIMIT 1",
|
|
(session_id,),
|
|
).fetchone()
|
|
return dict(row) if row else None
|
|
|
|
|
|
def get_readings(session_id: int, minutes: int = 60, probe_number: int = None):
|
|
with get_connection() as conn:
|
|
if probe_number is not None:
|
|
column = f"probe{probe_number}_temp"
|
|
rows = conn.execute(
|
|
f"""
|
|
SELECT received_at, {column} AS temp, probe{probe_number}_connected AS connected
|
|
FROM readings
|
|
WHERE session_id = ?
|
|
AND received_at > datetime('now', '-{minutes} minutes')
|
|
AND {column} IS NOT NULL
|
|
ORDER BY received_at ASC
|
|
""",
|
|
(session_id,),
|
|
).fetchall()
|
|
else:
|
|
rows = conn.execute(
|
|
f"""
|
|
SELECT received_at,
|
|
probe1_temp, probe1_connected,
|
|
probe2_temp, probe2_connected,
|
|
probe3_temp, probe3_connected,
|
|
probe4_temp, probe4_connected
|
|
FROM readings
|
|
WHERE session_id = ?
|
|
AND received_at > datetime('now', '-{minutes} minutes')
|
|
ORDER BY received_at ASC
|
|
""",
|
|
(session_id,),
|
|
).fetchall()
|
|
return [dict(row) for row in rows]
|
|
|
|
|
|
def set_setpoint(session_id: int, probe_number: int, label: str, target_temp: float):
|
|
with get_connection() as conn:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO setpoints (session_id, probe_number, label, target_temp)
|
|
VALUES (?, ?, ?, ?)
|
|
ON CONFLICT(session_id, probe_number) DO UPDATE SET
|
|
label = excluded.label,
|
|
target_temp = excluded.target_temp,
|
|
created_at = excluded.created_at
|
|
""",
|
|
(session_id, probe_number, label, target_temp),
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def get_setpoints(session_id: int):
|
|
with get_connection() as conn:
|
|
rows = conn.execute(
|
|
"SELECT * FROM setpoints WHERE session_id = ? ORDER BY probe_number",
|
|
(session_id,),
|
|
).fetchall()
|
|
return [dict(row) for row in rows]
|