import os import subprocess import time from urllib.parse import urlparse import requests from modules import ( app_cellular, app_gps, app_health, app_network, app_obd, app_remote_config, app_upload, ) SERVICE_UNITS = ( ("Web app", "yaris-obd.service"), ("NetworkManager", "NetworkManager.service"), ("Hotspot watcher", "yaris-netwatch.timer"), ("Phone uplink wait", "yaris-uplink-wait.service"), ) def parse_systemctl_show(text): values = {} for line in text.splitlines(): key, separator, value = line.partition("=") if separator: values[key] = value restarts = values.get("NRestarts", "") return { "load_state": values.get("LoadState", "unknown"), "active_state": values.get("ActiveState", "unknown"), "sub_state": values.get("SubState", "unknown"), "unit_file_state": values.get("UnitFileState", "unknown"), "result": values.get("Result", "unknown"), "restarts": int(restarts) if restarts.isdigit() else None, } def _systemd_unit_status(label, unit): try: result = subprocess.run( [ "systemctl", "show", unit, "--property=LoadState,ActiveState,SubState,UnitFileState,Result,NRestarts", ], capture_output=True, text=True, timeout=5, check=True, ) except (OSError, subprocess.SubprocessError) as error: return { "label": label, "unit": unit, "ok": False, "error": str(error), } status = parse_systemctl_show(result.stdout) status.update( { "label": label, "unit": unit, "ok": True, } ) return status def _server_status(): credentials = app_remote_config.server_credentials() server_url = credentials["server_url"] device_name = credentials["device_name"] api_key = credentials["api_key"] parsed = urlparse(server_url) if server_url else None status = { "configured": bool(server_url), "server_url": server_url, "host": parsed.netloc if parsed else "", "device_name": device_name, "api_key_configured": bool(api_key), "reachable": False, "authenticated": False, "latency_ms": None, "health_http_status": None, "error": None, } if not server_url: status["error"] = "Server URL is not configured." return status started = time.monotonic() try: health_response = requests.get(f"{server_url}/health", timeout=5) except requests.RequestException as error: status["error"] = f"Could not reach server: {error}" return status status["latency_ms"] = round((time.monotonic() - started) * 1000) status["health_http_status"] = health_response.status_code if health_response.ok: status["reachable"] = True else: status["error"] = f"/health returned HTTP {health_response.status_code}." return status if not device_name or not api_key: status["error"] = "Device credentials are not fully configured yet." return status try: auth_response = requests.get( f"{server_url}/api/device-config", headers={ "X-Device-Name": device_name, "X-Api-Key": api_key, }, timeout=5, ) except requests.RequestException as error: status["error"] = f"Server reachable, but auth check failed: {error}" return status if auth_response.status_code == 200: status["authenticated"] = True return status if auth_response.status_code == 401: status["error"] = "Server reachable, but device credentials were rejected." return status status["error"] = ( f"Server reachable, but config auth returned HTTP {auth_response.status_code}." ) return status def _cellular_hat_status(): serial_port = app_gps.SERIAL_PORT serial_port_present = os.path.exists(serial_port) module_responding = False if serial_port_present: with app_gps._serial_lock: module_responding = app_gps._module_responds() return { "serial_port": serial_port, "serial_port_present": serial_port_present, "module_responding": module_responding, "configured": app_cellular.is_configured(), "apn_configured": bool(app_cellular.get_apn()), } def _obd_status(): port = app_obd._find_obd_port() connected = app_obd._is_car_connected() return { "adapter_present": bool(port), "port": port, "car_connected": connected, "status_label": "Connected" if connected else "Disconnected", } def collect_status(): try: uplink = {"ok": True, **app_network.get_status()} except app_network.NetworkConfigurationError as error: uplink = {"ok": False, "error": str(error)} return { "generated_at": time.time(), "health": app_health.collect_health(), "gps": app_gps.get_status(), "uplink": uplink, "server": _server_status(), "upload": app_upload.get_status(), "cellular_hat": _cellular_hat_status(), "obd": _obd_status(), "services": [ _systemd_unit_status(label, unit) for label, unit in SERVICE_UNITS ], }