Files
yaris-obd/app.py
T
2026-08-10 18:40:22 -04:00

354 lines
12 KiB
Python

from flask import Flask, render_template, make_response, json, jsonify, request, send_file, Response
from modules import app_gps, app_events, app_car, app_account, app_obd, app_sql, app_dash, app_upload, app_video, app_health, app_alerts, app_settings, app_network, app_device_config, app_device_sync, app_remote_config, app_status
import os, time
from gevent.pywsgi import WSGIServer
app = Flask(__name__)
app_events.socketio.init_app(app)
account = app_account.Account()
obd = app_obd.start_obd()
car = app_car.Car()
gps = app_gps.start_gps()
app_sql.start_logging()
app_upload.start_uploading()
app_alerts.start_alerting()
def _reload_runtime_device_config():
account.reload_runtime_config()
car.reload_runtime_config()
app_device_sync.register_reload_callback(_reload_runtime_device_config)
app_device_sync.start_sync()
@app.route('/')
def index():
return render_template('index.html', account=account, car=car)
@app.route('/dtc')
def dtc():
return render_template('dtc.html', account = account, car = car)
@app.route('/dash')
def dash():
return render_template('dash.html', account = account, car = car)
@app.route('/history')
def history():
return render_template('history.html', account=account, car=car)
@app.route('/settings')
def settings():
return render_template('settings.html', account=account, car=car)
@app.route('/status')
def status_page():
return render_template('status.html', account=account, car=car)
@app.route('/api/settings', methods=['GET', 'POST'])
def api_settings():
if request.method == 'GET':
return jsonify(app_settings.get_settings())
errors = app_settings.update_settings(request.get_json(silent=True))
if errors:
return jsonify(error="Invalid settings.", fields=errors), 400
return jsonify(app_settings.get_settings())
@app.route('/api/device-config', methods=['GET', 'POST'])
def api_device_config():
if request.method == 'GET':
return jsonify(app_device_config.get_device_config())
errors = {}
app_device_config.update_device_config(request.get_json(silent=True), errors)
if errors:
return jsonify(error="Invalid device config.", fields=errors), 400
_reload_runtime_device_config()
return jsonify(app_device_config.get_device_config())
@app.route('/api/remote-config', methods=['GET', 'POST'])
def api_remote_config():
if request.method == 'GET':
return jsonify(app_remote_config.get_remote_config())
errors = {}
app_remote_config.update_remote_config(request.get_json(silent=True), errors)
if errors:
return jsonify(error="Invalid remote config.", fields=errors), 400
app_upload.start_uploading()
app_device_sync.start_sync()
return jsonify(app_remote_config.get_remote_config())
@app.route('/api/remote-config/test', methods=['POST'])
def api_remote_config_test():
result = app_remote_config.test_connection()
return jsonify(result), 200 if result.get("ok") else 400
@app.route('/api/remote-config/sync-now', methods=['POST'])
def api_remote_config_sync_now():
try:
result = app_device_sync.sync_now()
except Exception as error:
return jsonify(status="error", message=str(error)), 502
status_codes = {
"synced": 200,
"no_change": 200,
"not_configured": 400,
"offline": 503,
}
return jsonify(result), status_codes.get(result["status"], 500)
@app.route('/api/uplink/status')
def uplink_status():
try:
return jsonify(app_network.get_status())
except app_network.NetworkConfigurationError as error:
return jsonify(error=str(error)), 503
@app.route('/api/uplink/config', methods=['POST', 'DELETE'])
def uplink_config():
try:
if request.method == 'DELETE':
app_network.remove_phone_hotspot()
else:
body = request.get_json(silent=True)
if not isinstance(body, dict):
return jsonify(error="Expected a JSON object."), 400
app_network.configure_phone_hotspot(
body.get("ssid"),
body.get("password"),
)
return jsonify(app_network.get_status())
except app_network.NetworkConfigurationError as error:
return jsonify(error=str(error)), 400
@app.route('/api/uplink/connect', methods=['POST'])
def uplink_connect():
try:
app_network.request_phone_connection()
except app_network.NetworkConfigurationError as error:
return jsonify(error=str(error)), 503
return jsonify(status="connection_requested"), 202
@app.route('/api/trips')
def trips():
return jsonify({"trips": app_sql.list_trips()})
@app.route('/api/gps/status')
def gps_status():
return jsonify(app_gps.get_status())
@app.route('/api/trip/<int:trip_id>')
def trip_route(trip_id):
trip = app_sql.get_trip_route(trip_id)
if trip is None:
return jsonify(error=f"Trip not found: {trip_id}"), 404
return jsonify(trip)
import functools
import urllib.request
@functools.lru_cache(maxsize=512)
def _geocode_cached(lat4, lon4):
"""Try Nominatim first (street-level), fall back to reverse_geocoder (city)."""
lat = lat4 / 10000.0
lon = lon4 / 10000.0
try:
url = f"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lon}&accept-language=en"
req = urllib.request.Request(url, headers={"User-Agent": "yaris-obd/1.0"})
with urllib.request.urlopen(req, timeout=4) as resp:
data = json.loads(resp.read())
a = data.get("address", {})
road = a.get("road") or a.get("hamlet") or a.get("suburb") or ""
city = a.get("city") or a.get("town") or a.get("village") or a.get("municipality") or ""
label = ", ".join(p for p in [road, city] if p)
if label:
return label
except Exception:
pass
# Offline fallback
import reverse_geocoder as rg
results = rg.search([(lat, lon)], mode=1, verbose=False)
if results:
r = results[0]
return ", ".join(p for p in [r.get("name", ""), r.get("admin1", "")] if p)
return f"{lat:.5f}, {lon:.5f}"
@app.route('/api/geocode')
def geocode_route():
try:
lat = float(request.args['lat'])
lon = float(request.args['lon'])
except (KeyError, ValueError):
return jsonify(error="lat and lon required"), 400
# Round to 3 decimals (~111m precision) for cache key
label = _geocode_cached(int(round(lat, 4) * 10000), int(round(lon, 4) * 10000))
return jsonify(label=label)
@app.route('/api/dash/devices')
def dash_devices():
devices = app_dash.list_usb_storage_partitions()
return jsonify({"devices": devices})
@app.route('/api/dash/import', methods=['POST'])
def dash_import():
body = request.get_json(silent=True)
relative_paths = body.get("paths") if isinstance(body, dict) else None
started, err = app_dash.start_import_async(relative_paths=relative_paths)
if not started:
return jsonify({"status": "busy", "message": err}), 409
return jsonify({"status": "started"})
@app.route('/api/dash/import/status')
def dash_import_status():
job = app_dash.get_import_status()
if job is None:
return jsonify({"status": "none"})
return jsonify(job)
@app.route('/api/dash/files')
def dash_files():
summary = app_dash.list_dashcam_files()
return jsonify(summary)
@app.route('/api/dash/files', methods=['DELETE'])
def dash_delete_all():
summary = app_dash.delete_dashcam_files(delete_all=True)
status_code = 200 if summary["status"] in {"ok", "partial"} else 409
return jsonify(summary), status_code
@app.route('/api/dash/file', methods=['DELETE'])
def dash_delete_file():
body = request.get_json(silent=True) or {}
summary = app_dash.delete_dashcam_files(relative_path=body.get("path"))
status_codes = {
"ok": 200,
"partial": 409,
"invalid_path": 400,
"not_found": 404,
}
return jsonify(summary), status_codes.get(summary["status"], 409)
@app.route('/api/local-videos')
def local_videos():
return jsonify(app_video.list_local_videos())
@app.route('/api/local-video/file')
def local_video_file():
relative_path = request.args.get("path", "")
try:
path = app_video.get_local_video_path(relative_path)
except ValueError as error:
return jsonify(error=str(error)), 400
except FileNotFoundError:
return jsonify(error=f"Local video not found: {relative_path}"), 404
as_attachment = request.args.get("download") == "1"
return send_file(path, conditional=True, as_attachment=as_attachment)
@app.route('/api/dash/file/download')
def dash_file_download():
relative_path = request.args.get("path", "")
try:
path = app_dash.safe_video_path(app_dash.DEFAULT_MOUNT_POINT, relative_path)
except (ValueError, FileNotFoundError) as error:
return jsonify(error=str(error)), 400
return send_file(path, as_attachment=True)
@app.route('/api/local-video/thumbnail')
def local_video_thumbnail():
relative_path = request.args.get("path", "")
try:
path = app_video.get_thumbnail_path(relative_path)
except ValueError as error:
return jsonify(error=str(error)), 400
except FileNotFoundError:
return jsonify(error=f"Local video not found: {relative_path}"), 404
except RuntimeError as error:
if str(error) == "generating":
return jsonify(status="generating"), 202
return jsonify(error=str(error)), 422
return send_file(path, conditional=True, mimetype="image/jpeg")
@app.route('/api/local-video', methods=['DELETE'])
def local_video_delete():
body = request.get_json(silent=True) or {}
summary = app_video.delete_local_video(body.get("path"))
status_codes = {
"ok": 200,
"invalid_path": 400,
"not_found": 404,
"busy": 409,
"delete_failed": 409,
}
return jsonify(summary), status_codes.get(summary["status"], 409)
@app.route('/api/local-video/convert', methods=['POST'])
def local_video_convert():
body = request.get_json(silent=True) or {}
path = body.get("path", "")
if not path:
return jsonify({"status": "invalid_path", "message": "No path provided."}), 400
queued = app_video.queue_convert(path, force=True)
if queued is False:
return jsonify({"status": "busy", "message": "Already converting."}), 409
return jsonify({"status": "started"})
@app.route('/api/upload/status')
def upload_status():
return jsonify(app_upload.get_status())
@app.route('/api/health')
def health():
return jsonify(app_health.collect_health())
@app.route('/api/status')
def api_status():
return jsonify(app_status.collect_status())
@app.route('/api/status/restart-app', methods=['POST'])
def api_status_restart_app():
try:
return jsonify(app_status.request_app_restart()), 202
except RuntimeError as error:
return jsonify(error=str(error)), 503
@app.route('/api/alerts/status')
def alert_status():
return jsonify(app_alerts.get_status())
@app.route('/api/upload/dashcam_toggle', methods=['POST'])
def upload_dashcam_toggle():
body = request.get_json(silent=True) or {}
if not isinstance(body.get("enabled"), bool):
return jsonify(error="expected JSON body {'enabled': true|false}"), 400
app_upload.set_dashcam_upload_enabled(body["enabled"])
return jsonify(app_upload.get_status())
# not sure if im gonna use this
@app.route('/api/obd_speed/')
def data():
data = app_obd.speed
response = make_response(json.dumps(data))
response.content_type = 'application/json'
return response
@app.route('/api/dtc')
def api_dtc():
return jsonify({
"mil": app_obd.get_fresh_mil(),
"codes": app_obd.get_fresh_dtc_codes(),
})
@app.route('/api/stats')
def api_stats():
return jsonify(app_sql.get_overview_stats())
if __name__ == '__main__':
# Debug/Development
# app.run(debug=True, host="0.0.0.0", port="5000")
# Production
http_server = WSGIServer(('', 5000), app)
print("server on http://localhost:5000")
http_server.serve_forever()