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/') 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) @app.route('/api/trip/', methods=['DELETE']) def api_delete_trip(trip_id): deleted = app_sql.delete_trip(trip_id) if not deleted: return jsonify(error=f"Trip not found: {trip_id}"), 404 return jsonify(status="deleted"), 200 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/geocode/batch', methods=['POST']) def geocode_batch_route(): body = request.get_json(silent=True) or {} coords = body.get("coords") if not isinstance(coords, list) or not coords: return jsonify(error="expected {coords: [{lat, lon}, ...]}"), 400 result = {} for item in coords: try: lat = float(item["lat"]) lon = float(item["lon"]) except (KeyError, ValueError, TypeError): continue lat4 = int(round(lat, 4) * 10000) lon4 = int(round(lon, 4) * 10000) label = _geocode_cached(lat4, lon4) if label: result[f"{lat:.3f},{lon:.3f}"] = label return jsonify(result) @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(): fresh_mil = app_obd.get_fresh_mil() fresh_codes = app_obd.get_fresh_dtc_codes() if fresh_mil is None and not fresh_codes: # OBD offline/stale (disconnected or not yet polled): fall back to the # last-persisted DTC snapshot so codes survive a disconnect/reboot. persisted = app_sql.get_persisted_dtc_state() fresh_mil = persisted["mil"] fresh_codes = persisted["codes"] return jsonify({ "mil": fresh_mil, "codes": fresh_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()