132 lines
4.9 KiB
Python
132 lines
4.9 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", ""),
|
|
"odometer_baseline": float(app_sql.get_setting("odometer_baseline", "0") or "0"),
|
|
"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:
|
|
# Allow toggling sync_enabled off
|
|
if "sync_enabled" in body and not body["sync_enabled"]:
|
|
_set_sync_enabled(False)
|
|
# Toggling off is always allowed — don't error on locked fields
|
|
# (the JS always sends them alongside the toggle).
|
|
return errors
|
|
# Sync is staying enabled — locked fields are ignored silently.
|
|
# The JS always sends them; rejecting would break the save flow.
|
|
if "sync_enabled" in body and body["sync_enabled"]:
|
|
return errors
|
|
# User tried to edit locked fields without the sync toggle — reject.
|
|
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."
|
|
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."
|
|
elif body["sync_enabled"]:
|
|
from . import app_remote_config
|
|
|
|
remote = app_remote_config.get_remote_config()
|
|
if not remote["server_url"] or not remote["device_name"] or not remote["api_key_configured"]:
|
|
errors["sync_enabled"] = "Configure server URL, device name, and API key before enabling server sync."
|
|
else:
|
|
_set_sync_enabled(body["sync_enabled"])
|
|
|
|
if "sync_enabled" in body and body["sync_enabled"] and "sync_enabled" not in errors:
|
|
_set_sync_enabled(True)
|
|
|
|
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")
|