feat: odometer baseline syncs to server — push on save, pull on sync, +/api/device-config/push endpoint
This commit is contained in:
@@ -16,6 +16,7 @@ def get_device_config():
|
|||||||
"car_model": app_sql.get_setting("device_car_model", ""),
|
"car_model": app_sql.get_setting("device_car_model", ""),
|
||||||
"device_name": app_sql.get_setting("device_device_name", ""),
|
"device_name": app_sql.get_setting("device_device_name", ""),
|
||||||
"avatar_path": app_sql.get_setting("device_avatar_path", ""),
|
"avatar_path": app_sql.get_setting("device_avatar_path", ""),
|
||||||
|
"odometer_baseline": float(app_sql.get_setting("odometer_baseline", "0") or "0"),
|
||||||
"sync_enabled": sync_enabled,
|
"sync_enabled": sync_enabled,
|
||||||
}
|
}
|
||||||
# Fill in any missing values from config file or defaults
|
# Fill in any missing values from config file or defaults
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ def _store_synced_config(config):
|
|||||||
app_sql.set_setting("device_avatar_path", config.get("avatar_path", "img/yaris.png"))
|
app_sql.set_setting("device_avatar_path", config.get("avatar_path", "img/yaris.png"))
|
||||||
app_sql.set_setting("device_config_version", str(config.get("config_version", 0)))
|
app_sql.set_setting("device_config_version", str(config.get("config_version", 0)))
|
||||||
app_sql.set_setting("device_config_synced_at", str(int(time.time())))
|
app_sql.set_setting("device_config_synced_at", str(int(time.time())))
|
||||||
|
if config.get("odometer_baseline") is not None:
|
||||||
|
app_sql.set_setting("odometer_baseline", str(config["odometer_baseline"]))
|
||||||
|
|
||||||
|
|
||||||
def _on_config_changed():
|
def _on_config_changed():
|
||||||
|
|||||||
@@ -220,4 +220,29 @@ def update_settings(body):
|
|||||||
settings["alert_ntfy_token"] = ntfy_token
|
settings["alert_ntfy_token"] = ntfy_token
|
||||||
|
|
||||||
app_sql.set_settings(settings)
|
app_sql.set_settings(settings)
|
||||||
|
|
||||||
|
# Push odometer_baseline to server if configured and non-zero
|
||||||
|
if values["odometer_baseline"] is not None and values["odometer_baseline"] > 0:
|
||||||
|
_push_odometer_to_server(values["odometer_baseline"])
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _push_odometer_to_server(baseline):
|
||||||
|
try:
|
||||||
|
from . import app_upload
|
||||||
|
creds = app_upload._credentials()
|
||||||
|
if not creds["server_url"] or not creds["device_name"] or not creds["api_key"]:
|
||||||
|
return
|
||||||
|
import requests
|
||||||
|
requests.post(
|
||||||
|
f"{creds['server_url']}/api/device-config/push",
|
||||||
|
headers={
|
||||||
|
"X-Device-Name": creds["device_name"],
|
||||||
|
"X-Api-Key": creds["api_key"],
|
||||||
|
},
|
||||||
|
json={"odometer_baseline": baseline},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -781,6 +781,22 @@ def api_device_config_get():
|
|||||||
return jsonify(config)
|
return jsonify(config)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/device-config/push")
|
||||||
|
@require_device_auth
|
||||||
|
def api_device_config_push():
|
||||||
|
"""Update device configuration from the Pi (device-authenticated).
|
||||||
|
|
||||||
|
Pi pushes local config changes to server. Accepts a partial update:
|
||||||
|
any of owner_name, car_model, device_name, avatar_path, odometer_baseline.
|
||||||
|
"""
|
||||||
|
body = request.get_json(silent=True) or {}
|
||||||
|
success, errors = device_config.update_device_config(g.device_id, body)
|
||||||
|
if not success:
|
||||||
|
return jsonify(error="Invalid device config.", fields=errors), 400
|
||||||
|
config = device_config.get_device_config(g.device_id)
|
||||||
|
return jsonify(config)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/v1/device-config/ack")
|
@app.post("/api/v1/device-config/ack")
|
||||||
@require_device_auth
|
@require_device_auth
|
||||||
def api_device_config_ack():
|
def api_device_config_ack():
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ def get_device_config(device_id):
|
|||||||
"car_model": dev.get("car_model") or "",
|
"car_model": dev.get("car_model") or "",
|
||||||
"device_name": dev.get("device_name") or "",
|
"device_name": dev.get("device_name") or "",
|
||||||
"avatar_path": dev.get("avatar_path") or "img/yaris.png",
|
"avatar_path": dev.get("avatar_path") or "img/yaris.png",
|
||||||
|
"odometer_baseline": float(dev.get("odometer_baseline") or 0),
|
||||||
"config_version": dev.get("config_version") or 0,
|
"config_version": dev.get("config_version") or 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,51 +42,44 @@ def update_device_config(device_id, data):
|
|||||||
car_model = data.get("car_model", "").strip() if "car_model" in data else None
|
car_model = data.get("car_model", "").strip() if "car_model" in data else None
|
||||||
device_name = data.get("device_name", "").strip() if "device_name" in data else None
|
device_name = data.get("device_name", "").strip() if "device_name" in data else None
|
||||||
avatar_path = data.get("avatar_path", "").strip() if "avatar_path" in data else None
|
avatar_path = data.get("avatar_path", "").strip() if "avatar_path" in data else None
|
||||||
|
odometer_baseline = data.get("odometer_baseline") if "odometer_baseline" in data else None
|
||||||
|
|
||||||
if owner_name is not None and len(owner_name) > 100:
|
if owner_name is not None and len(owner_name) > 100:
|
||||||
errors["owner_name"] = "Owner name must be 1-100 characters."
|
errors["owner_name"] = "Owner name must be 1-100 characters."
|
||||||
|
|
||||||
if car_model is not None and len(car_model) > 100:
|
if car_model is not None and len(car_model) > 100:
|
||||||
errors["car_model"] = "Car model must be 1-100 characters."
|
errors["car_model"] = "Car model must be 1-100 characters."
|
||||||
|
|
||||||
if device_name is not None and len(device_name) > 100:
|
if device_name is not None and len(device_name) > 100:
|
||||||
errors["device_name"] = "Device name must be 1-100 characters."
|
errors["device_name"] = "Device name must be 1-100 characters."
|
||||||
|
|
||||||
if avatar_path is not None and len(avatar_path) > 255:
|
if avatar_path is not None and len(avatar_path) > 255:
|
||||||
errors["avatar_path"] = "Avatar path must be 1-255 characters."
|
errors["avatar_path"] = "Avatar path must be 1-255 characters."
|
||||||
|
if odometer_baseline is not None:
|
||||||
|
try:
|
||||||
|
odometer_baseline = float(odometer_baseline)
|
||||||
|
if odometer_baseline < 0 or odometer_baseline > 999999:
|
||||||
|
errors["odometer_baseline"] = "Must be between 0 and 999,999 km."
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
errors["odometer_baseline"] = "Must be a number."
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
return False, errors
|
return False, errors
|
||||||
|
|
||||||
# Build update dict (only include provided fields)
|
|
||||||
with db.get_cursor(commit=True) as cur:
|
with db.get_cursor(commit=True) as cur:
|
||||||
# Increment config_version if any field changed
|
changed = owner_name is not None or car_model is not None or device_name is not None or avatar_path is not None or odometer_baseline is not None
|
||||||
if owner_name is not None or car_model is not None or device_name is not None or avatar_path is not None:
|
if changed:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"UPDATE devices SET config_version = config_version + 1, config_updated_at = now() WHERE id = %s",
|
"UPDATE devices SET config_version = config_version + 1, config_updated_at = now() WHERE id = %s",
|
||||||
(device_id,)
|
(device_id,)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update individual fields
|
|
||||||
if owner_name is not None:
|
if owner_name is not None:
|
||||||
cur.execute(
|
cur.execute("UPDATE devices SET owner_name = %s WHERE id = %s", (owner_name, device_id))
|
||||||
"UPDATE devices SET owner_name = %s WHERE id = %s",
|
|
||||||
(owner_name, device_id)
|
|
||||||
)
|
|
||||||
if car_model is not None:
|
if car_model is not None:
|
||||||
cur.execute(
|
cur.execute("UPDATE devices SET car_model = %s WHERE id = %s", (car_model, device_id))
|
||||||
"UPDATE devices SET car_model = %s WHERE id = %s",
|
|
||||||
(car_model, device_id)
|
|
||||||
)
|
|
||||||
if device_name is not None:
|
if device_name is not None:
|
||||||
cur.execute(
|
cur.execute("UPDATE devices SET device_name = %s WHERE id = %s", (device_name, device_id))
|
||||||
"UPDATE devices SET device_name = %s WHERE id = %s",
|
|
||||||
(device_name, device_id)
|
|
||||||
)
|
|
||||||
if avatar_path is not None:
|
if avatar_path is not None:
|
||||||
cur.execute(
|
cur.execute("UPDATE devices SET avatar_path = %s WHERE id = %s", (avatar_path, device_id))
|
||||||
"UPDATE devices SET avatar_path = %s WHERE id = %s",
|
if odometer_baseline is not None:
|
||||||
(avatar_path, device_id)
|
cur.execute("UPDATE devices SET odometer_baseline = %s WHERE id = %s", (odometer_baseline, device_id))
|
||||||
)
|
|
||||||
|
|
||||||
return True, {}
|
return True, {}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- Phase 27: Add odometer_baseline to devices table for server-side sync.
|
||||||
|
ALTER TABLE devices ADD COLUMN IF NOT EXISTS odometer_baseline DOUBLE PRECISION NOT NULL DEFAULT 0;
|
||||||
Reference in New Issue
Block a user