"""Device configuration sync from remote server (Phase 25.5a). Pi fetches device config from server when internet is available (connection-driven). Stores in SQLite and hot-reloads app globals if config changes. """ import os import subprocess import threading import time import requests from dotenv import load_dotenv from . import app_network, app_sql _SYNC_THREAD = None _SYNC_STOP = False _SYNC_CONFIG_VERSION = 0 # Track last-synced version to detect changes _RELOAD_CALLBACK = None _BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) load_dotenv(os.path.join(_BASE_DIR, "db", "upload_creds.env")) def _has_internet(): """Use the existing NetworkManager-derived status instead of probing 8.8.8.8 over HTTP.""" try: return app_network.get_status().get("internet_reachable", False) except ( OSError, subprocess.SubprocessError, ValueError, KeyError, app_network.NetworkConfigurationError, ): return False def _server_credentials(): server_url = os.getenv("YARIS_SERVER_URL", "").rstrip("/") device_name = os.getenv("YARIS_DEVICE_NAME") or os.getenv("YARIS_UPLOAD_DEVICE_NAME", "") device_key = os.getenv("YARIS_DEVICE_KEY") or os.getenv("YARIS_API_KEY", "") return server_url, device_name, device_key def start_sync(): """Start background device config sync thread.""" global _SYNC_THREAD, _SYNC_STOP if _SYNC_THREAD is not None: return _SYNC_STOP = False _SYNC_THREAD = threading.Thread(target=_sync_loop, daemon=True) _SYNC_THREAD.start() def register_reload_callback(callback): """Register a callback invoked after synced config is written locally.""" global _RELOAD_CALLBACK _RELOAD_CALLBACK = callback def stop_sync(): """Stop background device config sync thread.""" global _SYNC_STOP _SYNC_STOP = True def _sync_loop(): """Periodically sync device config from server when internet available.""" global _SYNC_CONFIG_VERSION server_url, device_name, device_key = _server_credentials() if not server_url or not device_name or not device_key: return sync_interval = 300 offline_interval = 60 while not _SYNC_STOP: try: if _has_internet(): response = requests.get( f"{server_url}/api/device-config", headers={ "X-Device-Name": device_name, "X-Api-Key": device_key, }, timeout=5, ) if response.status_code == 200: config = response.json() new_version = config.get("config_version", 0) if new_version != _SYNC_CONFIG_VERSION: _SYNC_CONFIG_VERSION = new_version _store_synced_config(config) _on_config_changed() time.sleep(sync_interval) else: time.sleep(offline_interval) except Exception as e: print(f"Device config sync error: {e}") time.sleep(60) def _store_synced_config(config): """Store synced config in SQLite database.""" app_sql.set_setting("device_owner_name", config.get("owner_name", "")) app_sql.set_setting("device_car_model", config.get("car_model", "")) app_sql.set_setting("device_device_name", config.get("device_name", "")) 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_synced_at", str(int(time.time()))) def _on_config_changed(): """Called when config changes and needs to be reloaded by app.""" if _RELOAD_CALLBACK is not None: _RELOAD_CALLBACK() def is_sync_enabled(): """Check if device config sync from server is enabled.""" server_url, device_name, device_key = _server_credentials() return bool(server_url) and bool(device_name) and bool(device_key) def get_synced_config_version(): """Get the last-synced config version number.""" return int(app_sql.get_setting("device_config_version", "0")) def get_synced_at_timestamp(): """Get timestamp of last successful sync.""" return app_sql.get_setting("device_config_synced_at", "")