"""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 . import app_network, app_sql _SYNC_THREAD = None _SYNC_STOP = False _SYNC_CONFIG_VERSION = 0 # Track last-synced version to detect changes 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 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 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 = os.getenv("YARIS_SERVER_URL", "") device_key = os.getenv("YARIS_DEVICE_KEY", "") if not server_url or not device_key: return # Sync disabled if server URL or device key not configured # Sync every 5 minutes (300 sec) when connected, backoff to 60 sec when offline sync_interval = 300 offline_interval = 60 while not _SYNC_STOP: try: is_connected = _has_internet() if is_connected: # Fetch device config from server headers = {"Authorization": f"Bearer {device_key}"} response = requests.get( f"{server_url.rstrip('/')}/api/device-config", headers=headers, timeout=5 ) if response.status_code == 200: config = response.json() # Check if config changed new_version = config.get("config_version", 0) if new_version != _SYNC_CONFIG_VERSION: _SYNC_CONFIG_VERSION = new_version # Store in database _store_synced_config(config) # Signal that config changed (for hot-reload) _on_config_changed() time.sleep(sync_interval) else: # Offline: backoff to less frequent checks time.sleep(offline_interval) except Exception as e: # Log error but don't crash sync thread 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.""" # This is a signal for future hot-reload implementation # For now, config is automatically read from database on each request # A full reload would require restarting the app or using a signal handler pass def is_sync_enabled(): """Check if device config sync from server is enabled.""" return bool(os.getenv("YARIS_SERVER_URL")) and bool(os.getenv("YARIS_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", "")