diff --git a/modules/app_settings.py b/modules/app_settings.py index fd05272..0799db5 100644 --- a/modules/app_settings.py +++ b/modules/app_settings.py @@ -37,6 +37,7 @@ def _number_value(setting_key, converter): def get_settings(): return { "dashcam_upload_enabled": app_upload.is_dashcam_upload_enabled(), + "trips_upload_enabled": app_upload.is_trips_upload_enabled(), "local_video_retention_days": app_video.retention_days(), "video_convert_enabled": app_video.is_convert_enabled(), "cellular_enabled": _setting_bool( @@ -99,6 +100,7 @@ def update_settings(body): values = {} for field in ( "dashcam_upload_enabled", + "trips_upload_enabled", "video_convert_enabled", "cellular_enabled", "ntfy_enabled", @@ -184,6 +186,9 @@ def update_settings(body): app_upload.DASHCAM_UPLOAD_SETTING_KEY: ( "1" if values["dashcam_upload_enabled"] else "0" ), + app_upload.TRIPS_UPLOAD_SETTING_KEY: ( + "1" if values["trips_upload_enabled"] else "0" + ), app_video.RETENTION_SETTING: str(values["local_video_retention_days"]), app_video.CONVERT_SETTING: "1" if values["video_convert_enabled"] else "0", "cellular_enabled": "1" if values["cellular_enabled"] else "0", diff --git a/modules/app_sql.py b/modules/app_sql.py index b6629e9..c5e6996 100644 --- a/modules/app_sql.py +++ b/modules/app_sql.py @@ -90,7 +90,9 @@ SQL_CREATE_TRIPS_TABLE = """CREATE TABLE IF NOT EXISTS trips ( DURATION_SECONDS integer NOT NULL, POINT_COUNT integer NOT NULL, AVERAGE_SPEED_KMH float NOT NULL DEFAULT 0, - MAX_SPEED_KMH float NOT NULL DEFAULT 0 + MAX_SPEED_KMH float NOT NULL DEFAULT 0, + START_ODOMETER float, + END_ODOMETER float );""" SQL_CREATE_TRIP_BUILD_STATE_TABLE = """CREATE TABLE IF NOT EXISTS trip_build_state ( @@ -180,6 +182,8 @@ def _migrate_trips_table(conn): additions = { "AVERAGE_SPEED_KMH": "float NOT NULL DEFAULT 0", "MAX_SPEED_KMH": "float NOT NULL DEFAULT 0", + "START_ODOMETER": "float", + "END_ODOMETER": "float", } for name, definition in additions.items(): if name not in existing_columns: @@ -271,7 +275,7 @@ def refresh_trips(force=False): if intersecting_trip[0] is not None: rebuild_start_id = min(rebuild_start_id, intersecting_trip[0]) rows = conn.execute( - """SELECT id, DATETIME, GPSX, GPSY, SPEED + """SELECT id, DATETIME, GPSX, GPSY, SPEED, ODOMETER FROM tracking WHERE id >= ? ORDER BY id ASC""", (rebuild_start_id,), ).fetchall() @@ -289,13 +293,15 @@ def refresh_trips(force=False): START_DATETIME, END_DATETIME, START_GPSX, START_GPSY, END_GPSX, END_GPSY, DISTANCE_KM, DURATION_SECONDS, POINT_COUNT, - AVERAGE_SPEED_KMH, MAX_SPEED_KMH + AVERAGE_SPEED_KMH, MAX_SPEED_KMH, + START_ODOMETER, END_ODOMETER ) VALUES ( :id, :start_tracking_id, :end_tracking_id, :start_datetime, :end_datetime, :start_gpsx, :start_gpsy, :end_gpsx, :end_gpsy, :distance_km, :duration_seconds, :point_count, - :average_speed_kmh, :max_speed_kmh + :average_speed_kmh, :max_speed_kmh, + :start_odometer, :end_odometer )""", trips, ) @@ -325,15 +331,25 @@ def get_overview_stats(): COALESCE(SUM(DURATION_SECONDS),0), COALESCE(MAX(MAX_SPEED_KMH),0) FROM trips """).fetchone() + odo_row = conn.execute(""" + SELECT MAX(ODOMETER) FROM tracking WHERE ODOMETER IS NOT NULL AND ODOMETER > 0 + """).fetchone() dtc_row = conn.execute(""" SELECT COUNT(*) FROM dtc_codes WHERE cleared_at IS NULL """).fetchone() if _table_exists(conn, "dtc_codes") else (0,) + from modules import app_video as _av + try: + video_count = sum(1 for _ in _av.list_local_videos().get("files", [])) + except Exception: + video_count = 0 return { "trip_count": row[0], "total_km": row[1], "total_duration_s": row[2], "all_time_max_speed": row[3], "active_dtc": dtc_row[0] if dtc_row else 0, + "video_count": video_count, + "odometer_km": odo_row[0] if odo_row and odo_row[0] else None, } @@ -353,21 +369,34 @@ def list_trips(): """SELECT id, START_DATETIME, END_DATETIME, START_GPSX, START_GPSY, END_GPSX, END_GPSY, DISTANCE_KM, DURATION_SECONDS, POINT_COUNT, - AVERAGE_SPEED_KMH, MAX_SPEED_KMH - FROM trips ORDER BY START_DATETIME DESC""" + AVERAGE_SPEED_KMH, MAX_SPEED_KMH, + START_ODOMETER, END_ODOMETER + FROM trips ORDER BY START_DATETIME ASC""" ).fetchall() - return [{ - "id": row[0], - "start_datetime": row[1], - "end_datetime": row[2], - "start": {"latitude": row[3], "longitude": row[4]}, - "end": {"latitude": row[5], "longitude": row[6]}, - "distance_km": row[7], - "duration_seconds": row[8], - "point_count": row[9], - "average_speed_kmh": row[10], - "max_speed_kmh": row[11], - } for row in rows] + # If real OBD odometer is available use it; otherwise fall back to cumulative distance + result = [] + cumulative_km = 0.0 + for row in rows: + dist = row[7] or 0 + start_odo = row[12] if row[12] is not None else round(cumulative_km, 1) + end_odo = row[13] if row[13] is not None else round(cumulative_km + dist, 1) + cumulative_km += dist + result.append({ + "id": row[0], + "start_datetime": row[1], + "end_datetime": row[2], + "start": {"latitude": row[3], "longitude": row[4]}, + "end": {"latitude": row[5], "longitude": row[6]}, + "distance_km": row[7], + "duration_seconds": row[8], + "point_count": row[9], + "average_speed_kmh": row[10], + "max_speed_kmh": row[11], + "odo_start_km": start_odo, + "odo_end_km": end_odo, + }) + result.reverse() # newest first for display + return result def get_trip_route(trip_id, max_points=2000): @@ -674,7 +703,7 @@ def _log_loop(): ) if should_log_tracking( position, speed, last_position, seconds_since_log - ) and insert_tracking(position[0], position[1], speed): + ) and insert_tracking(position[0], position[1], speed, app_obd.odometer): last_position = position last_logged_at = now diff --git a/modules/app_trip.py b/modules/app_trip.py index 4b121d8..a5b9ad0 100644 --- a/modules/app_trip.py +++ b/modules/app_trip.py @@ -46,6 +46,7 @@ def normalize_tracking_rows(rows): latitude = _number(row[2]) longitude = _number(row[3]) speed = _number(row[4]) or 0 + odometer = _number(row[5]) if len(row) > 5 else None if timestamp is None or latitude is None or longitude is None: continue if not (-90 <= latitude <= 90 and -180 <= longitude <= 180): @@ -56,6 +57,7 @@ def normalize_tracking_rows(rows): "latitude": latitude, "longitude": longitude, "speed": max(0, speed), + "odometer": odometer, }) return normalized @@ -202,6 +204,8 @@ def build_trip_summaries(raw_rows): "max_speed_kmh": round( max(obd_speeds or segment_speeds or [0]), 1 ), + "start_odometer": next((r["odometer"] for r in trip_rows if r.get("odometer")), None), + "end_odometer": next((r["odometer"] for r in reversed(trip_rows) if r.get("odometer")), None), }) return trips diff --git a/modules/app_upload.py b/modules/app_upload.py index 05e93b8..a5a61bf 100644 --- a/modules/app_upload.py +++ b/modules/app_upload.py @@ -66,6 +66,7 @@ BACKOFF_MAX = 300 DASHCAM_CHUNK_SIZE = 40 * 1024 * 1024 DASHCAM_UPLOAD_SETTING_KEY = "dashcam_upload_enabled" +TRIPS_UPLOAD_SETTING_KEY = "trips_upload_enabled" _upload_thread = None _last_error = None @@ -90,6 +91,11 @@ def set_dashcam_upload_enabled(enabled): app_sql.set_setting(DASHCAM_UPLOAD_SETTING_KEY, "1" if enabled else "0") +def is_trips_upload_enabled(): + """Defaults to enabled.""" + return app_sql.get_setting(TRIPS_UPLOAD_SETTING_KEY, "1") == "1" + + def get_status(): """Snapshot used by the /api/upload/status endpoint for the web UI.""" return { @@ -138,6 +144,8 @@ def _upload_tracking_batch(): modem's AT-command HTTP client before giving up. """ global _last_network_path + if not is_trips_upload_enabled(): + return 0 rows = app_sql.get_unsent_tracking_rows(limit=TRACKING_BATCH_SIZE) if not rows: diff --git a/static/js/history.js b/static/js/history.js index 6ed9a83..1b0b452 100644 --- a/static/js/history.js +++ b/static/js/history.js @@ -156,6 +156,7 @@ ${formatDistance(trip.distance_km)} ${formatDuration(trip.duration_seconds)} ${formatSpeed(trip.max_speed_kmh)} + ${trip.odo_start_km.toFixed(0)} → ${trip.odo_end_km.toFixed(0)} km