Files
yaris-obd/modules/app_device_config.py
T
Mathieu MoranandCopilot d7b9bb634f feat: add device config settings UI with optional server sync
- Add app_device_config.py module for managing device configuration (owner_name, car_model, avatar_path, device_name)
- Add sync_enabled flag to control whether Pi syncs config from server
- When sync is enabled, device config fields are read-only (server is source of truth)
- When sync is disabled, user can edit device config via Settings page
- Add /api/device-config endpoint (GET to retrieve, POST to update)
- Add Device identity section to settings.html with form fields for all config values
- Update settings.js to load/save device config with sync toggle
- Gracefully handles both config file (at startup) and database (live updates via settings UI)
- All 63 tests passing

Phase 25.5: Offline-first device config with eventual consistency. Config loads from JSON at startup (no network needed), can be edited via settings if not syncing, and will sync from server on next connectivity when Phase 25.5a server endpoints are implemented.

Co-authored-by: Copilot <[email protected]>
2026-08-10 16:41:45 -04:00

115 lines
4.0 KiB
Python

"""
Device configuration management (owner name, car model, avatar, sync status).
Stores in SQLite with option to sync from server (Phase 25.5a).
"""
import json
import os
from . import app_sql
def get_device_config():
"""Get current device config including sync status."""
sync_enabled = _get_sync_enabled()
config = {
"owner_name": app_sql.get_setting("device_owner_name", ""),
"car_model": app_sql.get_setting("device_car_model", ""),
"device_name": app_sql.get_setting("device_device_name", ""),
"avatar_path": app_sql.get_setting("device_avatar_path", ""),
"sync_enabled": sync_enabled,
}
# Fill in any missing values from config file or defaults
config = _apply_defaults(config)
return config
def update_device_config(body, errors):
"""Update device config from request body. Returns errors dict if invalid."""
if not isinstance(body, dict):
errors["error"] = "Invalid request body"
return errors
# Check if sync is enabled; if so, only allow sync-related updates
sync_enabled = _get_sync_enabled()
if sync_enabled:
# User cannot edit these fields if syncing from server
if any(k in body for k in ["owner_name", "car_model", "avatar_path"]):
errors["error"] = "Cannot edit device config while server sync is enabled. Server is source of truth."
# Allow toggling sync_enabled off
if "sync_enabled" in body and not body["sync_enabled"]:
_set_sync_enabled(False)
return errors
# Not syncing: allow all edits
if "owner_name" in body:
value = str(body["owner_name"]).strip()
if not value or len(value) > 100:
errors["owner_name"] = "Owner name must be 1-100 characters."
else:
app_sql.set_setting("device_owner_name", value)
if "car_model" in body:
value = str(body["car_model"]).strip()
if not value or len(value) > 100:
errors["car_model"] = "Car model must be 1-100 characters."
else:
app_sql.set_setting("device_car_model", value)
if "device_name" in body:
value = str(body["device_name"]).strip()
if not value or len(value) > 100:
errors["device_name"] = "Device name must be 1-100 characters."
else:
app_sql.set_setting("device_device_name", value)
if "avatar_path" in body:
value = str(body["avatar_path"]).strip()
if not value or len(value) > 255:
errors["avatar_path"] = "Avatar path must be 1-255 characters."
else:
app_sql.set_setting("device_avatar_path", value)
if "sync_enabled" in body:
if not isinstance(body["sync_enabled"], bool):
errors["sync_enabled"] = "Must be true or false."
else:
_set_sync_enabled(body["sync_enabled"])
return errors
def _apply_defaults(config):
"""Fill in any empty config values from config/device.json or hardcoded defaults."""
config_file_path = os.path.join(
os.path.dirname(os.path.dirname(__file__)), "config", "device.json"
)
file_config = {}
try:
if os.path.exists(config_file_path):
with open(config_file_path, "r") as f:
file_config = json.load(f)
except (IOError, json.JSONDecodeError):
pass
# Use file config as source of truth for initial values, fallback to hardcoded
defaults = {
"owner_name": "quoije",
"car_model": "Yaris 2013",
"device_name": "Yaris OBD",
"avatar_path": "img/yaris.png",
}
for key in defaults:
if not config[key]:
config[key] = file_config.get(key, defaults[key])
return config
def _get_sync_enabled():
"""Check if device config sync from server is enabled."""
return app_sql.get_setting("device_sync_enabled", "0") == "1"
def _set_sync_enabled(enabled):
"""Set device config sync status."""
app_sql.set_setting("device_sync_enabled", "1" if enabled else "0")