Files
ESP32-ThermoPro-Bridge/frontend/db.py
T
Keith Solomon 9e32b1b179 feat: add PWA frontend for CYD ThermoPro dashboard
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.
2026-07-20 20:14:48 -05:00

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]