Files

652 lines
34 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# rvlink_api.py (Refactored for Direct API Access with Expanded GET Endpoints)
import requests, json, re, hashlib
from datetime import datetime
from typing import Optional, List, Set, Dict, Any, Union # Keep for type hinting
# --- Add uvicorn import for direct running (if you keep it) ---
import uvicorn
# ---
from fastapi import FastAPI, HTTPException, status, Query
from fastapi.responses import StreamingResponse # Keep for file downloads
from pydantic import BaseModel, Field # Keep for request/response validation
# --- Configuration ---
BASE_URL = 'http://192.168.10.254' # Roof Unit IP
MBOX_CONFIG_URL = f'{BASE_URL}/cgi-bin/mbox-config' # Primary API endpoint
SYSTEM_URL = f'{BASE_URL}/cgi-bin/system-status' # another endpoint, this one was unauthenticated from the start...
# Constants for specific sections (can be used directly or in dedicated functions)
# These are useful for clarity if you build out more specific endpoint functions in FastAPI
GUIDE_CONFIG_SECTION = 'guide_config'
SCAN_SECTION = 'wifi_scan'
FW_VERSION_SECTION = 'firmware_version_get'
LTE_STATUS_SECTION = 'get_ec20_status_info'
HISTORY_SECTION = 'motor_home_list_get'
# REBOOT_SECTION would require specific handling for POST payload if it's system_reboot
# Example for a SET operation section (payload structure would be key)
# SYSTEM_SETTING_SECTION = 'system_setting' # Placeholder
# Define default headers for API calls
DEFAULT_ROOF_UNIT_HEADERS = {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Content-Type': 'application/json', # Assuming most POSTs send JSON
'User-Agent': 'RVLinkAPIDirectClient/2.1' # Updated User-Agent
}
# --- Pydantic Models ---
class ApiError(BaseModel):
errCode: int
errMsg: str
configDone: Optional[bool] = None
class RebootRequest(BaseModel):
delay: Optional[int] = Field(0, description="Delay in seconds before rebooting.")
# Add other reboot parameters if the API supports them
class ConnectWifiRequest(BaseModel):
ifname: str = Field(default="radio0", description="Wireless interface name (e.g., radio0 for 2.4GHz STA, radio1 for 5GHz STA if available)")
ssid: str = Field(..., description="SSID of the target Wi-Fi network")
bssid: str = Field(..., description="BSSID (MAC address) of the target Wi-Fi network (e.g., 'XX:XX:XX:XX:XX:XX')")
scanned_encryption: str = Field(..., description="Encryption type as reported by Wi-Fi scan (e.g., 'none', 'WPA2 PSK', 'WEP', 'WPA PSK/WPA2 PSK')")
key: Optional[str] = Field(None, description="Password for the Wi-Fi network (if required). Provide null or omit for open networks if scanned_encryption is 'none'.")
class ParsedLogEntry(BaseModel):
timestamp_str: str = Field(..., description="Original timestamp string from the log entry")
datetime_obj: Optional[datetime] = Field(None, description="Parsed datetime object (UTC or aware of timezone if possible)")
facility_level: str = Field(..., description="Facility and level (e.g., user.info, daemon.err)")
process_name: Optional[str] = Field(None, description="Name of the process generating the log")
pid: Optional[int] = Field(None, description="Process ID, if available")
message: str = Field(..., description="The actual log message content")
kernel_timestamp: Optional[float] = Field(None, description="Kernel-specific uptime timestamp, if applicable")
raw_log: str = Field(..., description="The original raw log line for reference")
log_hash: str = Field(..., description="SHA256 hash of the raw_log string for deduplication") # NEW FIELD
parse_error: Optional[str] = Field(None, description="Error message if parsing this specific line failed")
class ProcessedSystemLogResponse(BaseModel):
parsed_logs: List[ParsedLogEntry] = Field(..., description="List of parsed log entries")
processing_summary: Optional[str] = Field(None, description="Summary of the log processing operation")
processing_error: Optional[str] = Field(None, description="Error message if overall processing failed")
# Include original API fields from the device response
errCode: Optional[int] = Field(None, description="Original error code from the device")
errMsg: Optional[str] = Field(None, description="Original error message from the device")
configDone: Optional[bool] = Field(None, description="Original configDone flag from the device")
# Add other Pydantic models here if complex GET request bodies are needed for some sections
# Example:
# class GetDetailedReportRequest(BaseModel):
# report_type: str = Field(..., description="Type of report to generate")
# start_date: Optional[str] = Field(None, description="Start date for report (YYYY-MM-DD)")
# --- FastAPI App Initialization ---
app = FastAPI(
title="RVLink Direct API Proxy",
description="Provides a direct, simplified interface to the RVLink Roof Unit (MV2400) API, bypassing login/session requirements. Blessed by the Omnissiah.",
version="2.1.0",
)
# --- Core API Interaction Helper Functions (Refactored) ---
def invoke_roof_unit_api(
section_name: str,
http_method: str = "POST", # Most mbox-config calls are POST
query_method: str = "GET", # method in query string, "GET" or "SET"
endpoint: Optional[str] = "mbox-config",
payload: Optional[Dict[str, Any]] = None,
is_file_download: bool = False,
expected_content_type: Optional[str] = "application/json"
) -> Union[Dict[str, Any], StreamingResponse]:
"""
Generalized helper to invoke the roof unit's mbox-config.
Handles POST requests, JSON payloads, and file downloads.
"""
query_params = {"method": query_method, "section": section_name}
if endpoint == "system":
url = SYSTEM_URL
else:
url = MBOX_CONFIG_URL
actual_payload = payload if payload is not None else {}
current_headers = DEFAULT_ROOF_UNIT_HEADERS.copy()
if is_file_download:
current_headers['Accept'] = 'application/octet-stream, */*'
# Print statements for invocation clarity, as per the original script's style
print(f"Omnissiah's Invocation: HTTP {http_method} to {url}")
print(f" Query Litanies: {query_params}")
print(f" Sacred Headers: {current_headers}")
if http_method == "POST":
print(f" JSON Datagram: {json.dumps(actual_payload)}")
try:
if http_method == "POST":
response = requests.post(
url,
params=query_params,
json=actual_payload,
headers=current_headers,
timeout=30 if is_file_download else 10,
stream=is_file_download
)
elif http_method == "GET":
all_query_params = {**query_params, **(payload if payload else {})} # For true GETs, payload (if any) goes into query
# Note: For 'mbox-config', actual GETs are rare; it usually expects POST even for "get" operations.
# This branch is for completeness if a section *truly* uses HTTP GET.
print(f" (Actual HTTP GET to mbox-config with all_query_params: {all_query_params})")
_headers_for_get = current_headers.copy()
if 'Content-Type' in _headers_for_get and not actual_payload : # Remove Content-Type if no body for GET
del _headers_for_get['Content-Type']
response = requests.get(
url,
params=all_query_params,
headers=_headers_for_get,
timeout=10,
stream=is_file_download
)
else:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=f"HTTP method {http_method} not implemented for the Machine Spirit.")
response.raise_for_status() # Raises HTTPError for 4xx/5xx responses
if is_file_download:
original_filename = f"{section_name}.bin"
content_disposition = f'attachment; filename="{original_filename}"'
response_content_type = response.headers.get('Content-Type', 'application/octet-stream')
return StreamingResponse(response.iter_content(chunk_size=8192),
media_type=response_content_type,
headers={'Content-Disposition': content_disposition})
if not response.content:
return {"status": "success", "message": "Machine Spirit acknowledges. No data packet returned."}
if expected_content_type and expected_content_type.lower() not in response.headers.get('Content-Type', '').lower():
print(f"Warning: Unexpected Content-Type from Machine Spirit: {response.headers.get('Content-Type')}. Expected: {expected_content_type}. Raw response glimpse: {response.text[:200]}")
try:
return response.json()
except json.JSONDecodeError:
print(f"Warning: Non-JSON response from Machine Spirit at {url} (status {response.status_code}). Glimpse: {response.text[:200]}")
return {"status": "success", "raw_response": response.text, "message": "Machine Spirit responded with non-JSON data."}
except requests.exceptions.HTTPError as http_err:
error_detail_msg = f"Machine Spirit API error during '{section_name}' operation."
try:
error_content = http_err.response.json()
error_detail_msg = error_content.get("errMsg", json.dumps(error_content))
except:
error_detail_msg = http_err.response.text if http_err.response.text else error_detail_msg
print(f"HTTP error communing with {section_name}: {http_err.response.status_code} - {error_detail_msg}")
raise HTTPException(status_code=http_err.response.status_code, detail=error_detail_msg)
except requests.exceptions.RequestException as req_err:
print(f"Noospheric disturbance while calling {section_name}: {req_err}")
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=f"Roof unit communication error: {req_err}")
def parse_individual_log_line(log_line: str) -> ParsedLogEntry:
"""
Parses a single line of a system log.
Attempts to extract timestamp, facility/level, process, PID, and message.
Includes a SHA256 hash of the raw log line.
"""
# Calculate hash of the raw log line first
hasher = hashlib.sha256()
hasher.update(log_line.encode('utf-8')) # Always encode string to bytes before hashing
log_hash_hex = hasher.hexdigest()
entry_pattern = re.compile(
r"^(?P<timestamp_str>\w{3}\s+\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+"
r"(?P<facility_level>\S+?)\s+"
r"(?P<tag_and_message>.+)$"
)
match = entry_pattern.match(log_line)
if not match:
return ParsedLogEntry(
timestamp_str="",
facility_level="",
message=log_line,
raw_log=log_line,
log_hash=log_hash_hex, # Include hash even on parse error
parse_error="Primary regex failed to match line structure"
)
data = match.groupdict()
timestamp_str = data["timestamp_str"]
facility_level = data["facility_level"]
tag_and_message = data["tag_and_message"]
dt_obj = None
try:
dt_obj = datetime.strptime(timestamp_str, "%a %b %d %H:%M:%S %Y")
except ValueError:
pass
process_name: Optional[str] = None
pid: Optional[int] = None
message: str = ""
kernel_ts: Optional[float] = None
tag_message_split = tag_and_message.split(":", 1)
tag_part = tag_message_split[0].strip()
message = tag_message_split[1].strip() if len(tag_message_split) > 1 else ""
pid_match_simple = re.match(r"^(?P<proc_name>.+?)\[(?P<pid_val>\d+)\]$", tag_part)
if pid_match_simple:
process_name = pid_match_simple.group("proc_name")
pid = int(pid_match_simple.group("pid_val"))
else:
complex_tag_match = re.match(r"^(?P<proc_name>.+?)\s*\((?P<pid_like>[^)]+)\)$", tag_part)
if complex_tag_match:
potential_pid = complex_tag_match.group("pid_like")
if potential_pid.isdigit():
pid = int(potential_pid)
process_name = complex_tag_match.group("proc_name").strip()
else:
process_name = tag_part
else:
process_name = tag_part
if process_name == "kernel" and not message.startswith("["):
kernel_msg_match = re.match(r"\[\s*(?P<ts>\d+\.\d+)\s*\]\s*(?P<msg_body>.*)", message)
if kernel_msg_match:
kernel_ts = float(kernel_msg_match.group("ts"))
message = kernel_msg_match.group("msg_body").strip()
if process_name:
process_name = process_name.rstrip(':').strip()
return ParsedLogEntry(
timestamp_str=timestamp_str,
datetime_obj=dt_obj,
facility_level=facility_level,
process_name=process_name,
pid=pid,
message=message,
kernel_timestamp=kernel_ts,
raw_log=log_line,
log_hash=log_hash_hex # Assign the calculated hash
)
def process_device_system_log(raw_device_response: dict) -> ProcessedSystemLogResponse:
"""
Processes the raw JSON response containing the system log from the device.
"""
if not raw_device_response or \
"systemlog" not in raw_device_response or \
"systemlog" not in raw_device_response["systemlog"]:
return ProcessedSystemLogResponse(
parsed_logs=[],
processing_error="Invalid or missing systemlog data in device response",
errCode=raw_device_response.get("errCode"),
errMsg=raw_device_response.get("errMsg"),
configDone=raw_device_response.get("configDone")
)
log_string = raw_device_response["systemlog"]["systemlog"]
log_lines = log_string.strip().split('\n')
parsed_entries: List[ParsedLogEntry] = []
for line in log_lines:
if line.strip(): # Process non-empty lines
parsed_entries.append(parse_individual_log_line(line))
return ProcessedSystemLogResponse(
parsed_logs=parsed_entries,
processing_summary=f"Successfully parsed {len(parsed_entries)} log entries.",
errCode=raw_device_response.get("errCode"),
errMsg=raw_device_response.get("errMsg"),
configDone=raw_device_response.get("configDone")
)
# --- FastAPI Endpoints (Refactored to use direct API calls) ---
@app.get("/api/rvlink/action/scan_wifi", summary="Scan for WiFi Networks", response_model=Dict[str, Any], tags=["Actions"])
async def scan_wifi_networks(ifname: str = Query("radio0", description="Wireless interface for scanning (e.g., radio0 for 2.4GHz STA, radio1 for 5GHz STA if available)")):
"""Initiates a WiFi scan on the specified interface."""
# The wifi_scan section requires a payload: {"ifname": "radio0"}
return invoke_roof_unit_api(section_name=SCAN_SECTION, query_method="GET", payload={"ifname": ifname})
# --- Existing GET-like Endpoints (verified from your script) ---
@app.get("/api/rvlink/status/web_timeout_check", summary="No Idea...", response_model=Dict[str, Any], tags=["Status"])
async def get_web_timeout_check():
"""Retrieves LTE status information (EC20/EC25 module status)."""
return invoke_roof_unit_api(section_name="web_timeout_check", query_method="GET")
@app.get("/api/rvlink/status/internet", summary="Check internet connectivity", response_model=Dict[str, Any], tags=["Status"])
async def get_internet_status():
return invoke_roof_unit_api(section_name="internet_status", query_method="GET")
@app.get("/api/rvlink/status/lte", summary="Get LTE Status", response_model=Dict[str, Any], tags=["Status"])
async def get_lte_status():
"""Retrieves LTE status information (EC20/EC25 module status)."""
return invoke_roof_unit_api(section_name=LTE_STATUS_SECTION, query_method="GET")
@app.get("/api/rvlink/status/firmware_info", summary="Get Firmware Version", response_model=Dict[str, Any], tags=["Status"])
async def get_firmware_info():
"""Retrieves more detailed firmware info of the roof unit."""
return invoke_roof_unit_api(section_name="firmware_info", query_method="GET")
@app.get("/api/rvlink/status/uptime", summary="Get System Uptime", response_model=Dict[str, Any], tags=["Status"])
async def get_uptime():
"""Retrieves more detailed firmware info of the roof unit."""
return invoke_roof_unit_api(section_name="uptime_get", query_method="GET")
@app.get("/api/rvlink/status/firmware", summary="Get Firmware Version", response_model=Dict[str, Any], tags=["Status"])
async def get_firmware_version():
"""Retrieves the firmware version of the roof unit."""
return invoke_roof_unit_api(section_name=FW_VERSION_SECTION, query_method="GET")
@app.get("/api/rvlink/config/guide", summary="Get Guide Configuration", response_model=Dict[str, Any], tags=["Configuration"])
async def get_guide_config():
"""Retrieves the 'guide_config' from the roof unit."""
return invoke_roof_unit_api(section_name=GUIDE_CONFIG_SECTION, query_method="GET")
@app.get("/api/rvlink/history/connection_history", summary="Get Connection History", response_model=Dict[str, Any], tags=["History"])
async def get_motor_history():
"""Retrieves the 'motor_home_list_get' (movement history) from the roof unit."""
return invoke_roof_unit_api(section_name=HISTORY_SECTION, query_method="GET")
# --- NEW GET-like Endpoints (based on typical Comfast/OpenWrt APIs) ---
# Magos, verify these section_names against your CSV and add more as needed.
@app.get("/api/rvlink/status/system_usage", summary="Get System Resource Usage", response_model=Dict[str, Any], tags=["Status"])
async def get_system_usage():
"""Retrieves general system status (e.g., uptime, memory, CPU) from the roof unit."""
return invoke_roof_unit_api(section_name="system_usage", query_method="GET")
@app.get("/api/rvlink/status/ac_ap_status", summary="Get AC AP Status", response_model=Dict[str, Any], tags=["Status"])
async def get_ac_ap_status():
"""Retrieves not entirely sure"""
return invoke_roof_unit_api(section_name="ac_ap_status", query_method="GET")
@app.get("/api/rvlink/config/ac_enable_get", summary="Get AC Enable", response_model=Dict[str, Any], tags=["Configuration"])
async def get_ac_enable_get():
return invoke_roof_unit_api(section_name="ac_enable_get", query_method="GET")
@app.get("/api/rvlink/config/ac_group_config", summary="Get AC Group Config", response_model=Dict[str, Any], tags=["Configuration"])
async def get_ac_group_config():
return invoke_roof_unit_api(section_name="ac_group_config", query_method="GET")
@app.get("/api/rvlink/config/ac_list_get", summary="Get AC List", response_model=Dict[str, Any], tags=["Configuration"])
async def get_ac_list_get():
return invoke_roof_unit_api(section_name="ac_list_get", query_method="GET")
@app.get("/api/rvlink/config/add_default_group_config", summary="Sets up a default interface group config", response_model=Dict[str, Any], tags=["Configuration"])
async def add_default_group_config():
"""This function is designed to be safe to call multiple times. It only sets values if they are missing, so it won't overwrite existing group configuration."""
"""Ensures that there is always at least one group ("ac_group_default" with ID 1) in the system, and that the group ID counters and lists are initialized."""
return invoke_roof_unit_api(section_name="add_default_group_config", query_method="GET")
@app.get("/api/rvlink/config/lan", summary="Get LAN Configuration", response_model=Dict[str, Any], tags=["Configuration"])
async def get_lan_dhcp_config():
"""Retrieves config of just the lan interface from the roof unit."""
return invoke_roof_unit_api(section_name="lan_dhcp_config", query_method="GET")
@app.get("/api/rvlink/config/network_config", summary="Get LAN Configuration", response_model=Dict[str, Any], tags=["Configuration"])
async def get_network_config():
"""This is the full network config, including LAN and WAN settings."""
return invoke_roof_unit_api(section_name="network_config", query_method="GET")
@app.get("/api/rvlink/config/wan", summary="Get WAN Configuration", response_model=Dict[str, Any], tags=["Configuration"])
async def get_wan_config():
"""Retrieves WAN configuration from the roof unit."""
return invoke_roof_unit_api(section_name="wan_config", query_method="GET")
@app.get("/api/rvlink/config/mwan_capability_config", summary="Get mwan capabilities", response_model=Dict[str, Any], tags=["Configuration"])
async def get_mwan_capability_config():
"""Enumerates all network interfaces and their capabilities."""
return invoke_roof_unit_api(section_name="mwan_capability_config", query_method="GET")
@app.get("/api/rvlink/config/mwan_config", summary="Get mwan configuration", response_model=Dict[str, Any], tags=["Configuration"])
async def get_mwan_config():
""" This function appears to be the handler for retrieving detailed WAN (Wide Area Network) configuration settings"""
return invoke_roof_unit_api(section_name="mwan_config", query_method="GET")
@app.get("/api/rvlink/config/mwan_isp_define_config", summary="Get mwan isp config", response_model=Dict[str, Any], tags=["Configuration"])
async def get_mwan_isp_define_config():
""" I assume this has to due with the LTE modem, but I don't know what it does. """
return invoke_roof_unit_api(section_name="mwan_isp_define_config", query_method="GET")
@app.get("/api/rvlink/config/mwanportrule_config", summary="Get mwan port rule config (Multi-wan routing rules?)", response_model=Dict[str, Any], tags=["Configuration"])
async def get_mwanportrule_config():
""" I assume this has to due with the LTE modem, but I don't know what it does. """
return invoke_roof_unit_api(section_name="mwanportrule_config", query_method="GET")
@app.get("/api/rvlink/config/pptp_client_config", summary="Get PPTP configuration", response_model=Dict[str, Any], tags=["Configuration"])
async def get_pptp_client_config():
return invoke_roof_unit_api(section_name="pptp_client_config", query_method="GET")
@app.get("/api/rvlink/config/l2tp_client_config", summary="Get L2TP configuration", response_model=Dict[str, Any], tags=["Configuration"])
async def get_l2tp_client_config():
return invoke_roof_unit_api(section_name="l2tp_client_config", query_method="GET")
@app.get("/api/rvlink/config/multi_pppoe_macaddr_config", summary="Get MAC Address for PPPoE Config", response_model=Dict[str, Any], tags=["Configuration"])
async def get_multi_pppoe_macaddr_config():
return invoke_roof_unit_api(section_name="multi_pppoe_macaddr_config", query_method="GET")
@app.get("/api/rvlink/config/multi_pppoe", summary="Get PPPoE Config", response_model=Dict[str, Any], tags=["Configuration"])
async def get_multi_pppo():
return invoke_roof_unit_api(section_name="multi_pppoe", query_method="GET")
@app.get("/api/rvlink/Status/ap_wds_mode_ip_get", summary="Get WAN Status?", response_model=Dict[str, Any], tags=["Status"])
async def get_ap_wds_mode_ip_get():
return invoke_roof_unit_api(section_name="ap_wds_mode_ip_get", query_method="GET")
@app.get("/api/rvlink/config/mwan_qos", summary="Get mwan qos config", response_model=Dict[str, Any], tags=["Configuration"])
async def get_mwan_qo():
return invoke_roof_unit_api(section_name="mwan_qos", query_method="GET")
@app.get("/api/rvlink/config/qos_ip_limit", summary="Get QoS IP limiting config", response_model=Dict[str, Any], tags=["Configuration"])
async def get_qos_ip_limit():
return invoke_roof_unit_api(section_name="qos_ip_limit", query_method="GET")
@app.get("/api/rvlink/config/pptpd_user", summary="Get PPTP configured user/pw", response_model=Dict[str, Any], tags=["Configuration"])
async def get_pptpd_user():
return invoke_roof_unit_api(section_name="pptpd_user", query_method="GET")
@app.get("/api/rvlink/config/wifi_ap", summary="Get WiFi AP Settings", response_model=Dict[str, Any], tags=["Configuration", "WiFi"])
async def get_wifi_ap_settings(ifname: str = Query("radio0", description="AP interface name (e.g., radio0 for 2.4GHz AP, radio1 for 5GHz AP if dual-band)")):
"""Retrieves WiFi Access Point settings for a specific interface."""
# Magos: Verify 'wifi_ap_setting_get' is the correct section name from your CSV.
# Assumes it expects a payload like {"ifname": "value"} for the specific AP interface.
payload = {"ifname": ifname}
return invoke_roof_unit_api(section_name="wifi_ap_setting_get", query_method="GET", payload=payload)
@app.get("/api/rvlink/config/wifi_client", summary="Get WiFi Client Settings", response_model=Dict[str, Any], tags=["Configuration", "WiFi"])
async def get_wifi_client_settings(ifname: str = Query("radio0", description="STA interface name (e.g., radio0 for 2.4GHz STA, radio1 for 5GHz STA if dual-band)")):
"""Retrieves WiFi Client (STA) settings for a specific interface."""
# Magos: Verify 'wifi_client_setting_get' is the correct section name.
# Assumes it expects a payload like {"ifname": "value"} for the specific STA interface.
payload = {"ifname": ifname}
return invoke_roof_unit_api(section_name="wifi_client_setting_get", query_method="GET", payload=payload)
@app.get("/api/rvlink/clients/wlan", summary="Get Connected WLAN Clients", response_model=Dict[str, Any], tags=["Clients", "WiFi"])
async def get_wlan_clients():
"""Retrieves a list of clients connected to a specific WLAN AP interface."""
return invoke_roof_unit_api(section_name="wireless_user_list_get", query_method="GET")
@app.get("/api/rvlink/status/logs", summary="Get System Logs", response_model=Dict[str, Any], tags=["Status"])
async def get_logs():
"""Retrieves the current status of the positioning motor."""
return invoke_roof_unit_api(section_name="systemlog_get", query_method="GET")
@app.get("/api/rvlink/status/logs_processed", # Or modify your existing /systemlog endpoint
response_model=ProcessedSystemLogResponse,
tags=["System Monitoring"], # Or your relevant tag
summary="Get and process system logs from the roof unit",
description="Fetches the system log from the MV2400, parses it into a structured format, "
"and returns the list of processed log entries."
)
async def get_and_process_system_log():
# session = None # Manage your requests.Session appropriately
try:
raw_device_response = await get_logs() # Replace with actual call
# STEP 2: Process the fetched raw data
processed_data = process_device_system_log(raw_device_response)
if processed_data.processing_error:
# Decide how to handle top-level processing errors, e.g., raise HTTPException
# For now, it's included in the response object
pass
return processed_data
# except requests.exceptions.RequestException as e:
# # Handle exceptions from fetching data from the device
# raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY,
# detail=f"Error communicating with roof unit for system log: {str(e)}")
except Exception as e:
# Handle any other unexpected errors during processing
# You might want more specific error handling
return ProcessedSystemLogResponse(
parsed_logs=[],
processing_error=f"An unexpected error occurred: {str(e)}",
errCode=raw_device_response.get("errCode") if 'raw_device_response' in locals() else -1,
errMsg=raw_device_response.get("errMsg") if 'raw_device_response' in locals() else "Error",
configDone=raw_device_response.get("configDone") if 'raw_device_response' in locals() else False
)
# --- SET Operation Endpoints (existing) ---
@app.post("/api/rvlink/action/reboot", summary="Reboot Roof Unit", response_model=Dict[str, Any], tags=["Actions"])
async def reboot_roof_unit(reboot_params: Optional[RebootRequest] = None):
"""
Reboots the roof unit. The actual section name for reboot is 'system_reboot'.
Example payload for immediate reboot: {"delay": 0}
"""
payload = reboot_params.model_dump() if reboot_params else {"delay": 0}
return invoke_roof_unit_api(section_name="system_reboot", query_method="SET", payload=payload)
# need to configure 'http://192.168.10.254/cgi-bin/mbox-config?method=SET&section=motorhome_config_set' endpoint here
@app.post("/api/rvlink/action/connect_wifi", summary="Connect to a WiFi Network", response_model=Dict[str, Any], tags=["Actions"])
async def connect_to_wifi_network(wifi_params: ConnectWifiRequest):
"""
Connects the RVLink Roof Unit to a specified WiFi network.
"""
motorhome_payload_params = {
"ssid": wifi_params.ssid,
"bssid": wifi_params.bssid,
"encryption": "none",
"key": "" # Default to empty string
}
# Handle WEP key specifically if API differentiates 'key' and 'wep_key'
# The blobmsg_policy showed 'key' and 'wep_key'.
# If encryption is WEP, the key might need to be put into 'wep_key'.
# This assumes 'key' is the general field, but a device might be stricter.
# For now, this simplified model uses the 'key' field. If issues arise with WEP,
# this area may need refinement e.g.
# if api_encryption_type == "wep":
#     motorhome_payload_params["wep_key"] = motorhome_payload_params.pop("key")
# This depends on how the specific 'webmgnt' implementation handles these fields.
api_payload = {"motorhome": motorhome_payload_params}
return invoke_roof_unit_api(
section_name="motorhome_config_set",
query_method="SET",
http_method="POST",
payload=api_payload
)
# --- File Download Endpoint (existing) ---
@app.get("/api/rvlink/download/{section_name}",
summary="Download Configuration/Data File",
responses={
200: {"content": {"application/octet-stream": {}}, "description": "File stream"},
404: {"model": ApiError, "description": "Section/File not found"},
500: {"model": ApiError, "description": "Internal Server Error or Roof Unit Error"},
503: {"model": ApiError, "description": "Roof unit communication error"}
},
tags=["Files"])
async def download_file_from_section_direct(section_name: str):
"""
Downloads a file from the roof unit by specifying the 'section' name.
Example sections that might provide files:
- `system_config_backup`
- `log_package_download` (Hypothetical: for downloading a package of logs)
"""
print(f"FastAPI: Initiating direct file download for section: {section_name}")
try:
# This call assumes the 'section' for file download is invoked via POST (with method=GET in query)
# and the response is the file stream.
return invoke_roof_unit_api(section_name=section_name, query_method="GET", is_file_download=True)
except HTTPException as e:
print(f"FastAPI HTTPException during /download/{section_name}: {e.status_code} - {e.detail}")
raise e
except Exception as e:
print(f"FastAPI ERROR in /download/{section_name}: {type(e).__name__} - {e}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Unexpected error downloading from section '{section_name}': {str(e)}")
# --- System Endpoints ---
@app.get("/api/rvlink/system/get_led_status", summary="Get current state of LEDs on roof unit", response_model=Dict[str, Any], tags=["Status"])
async def get_led_status():
return invoke_roof_unit_api(section_name="led_status", query_method="GET", endpoint="system")
@app.get("/api/rvlink/system/language", summary="??", response_model=Dict[str, Any], tags=["Configuration"])
async def get_language():
return invoke_roof_unit_api(section_name="language", query_method="GET", endpoint="system")
@app.get("/api/rvlink/system/wifi_config", summary="??", response_model=Dict[str, Any], tags=["Configuration"])
async def get_wifi_config():
return invoke_roof_unit_api(section_name="wifi_config", query_method="GET", endpoint="system")
@app.get("/api/rvlink/system/lan_config", summary="??", response_model=Dict[str, Any], tags=["Configuration"])
async def get_lan_config():
return invoke_roof_unit_api(section_name="lan_config", query_method="GET", endpoint="system")
@app.get("/api/rvlink/system/mac_address", summary="??", response_model=Dict[str, Any], tags=["Configuration"])
async def get_mac_addr_get():
return invoke_roof_unit_api(section_name="mac_addr_get", query_method="GET", endpoint="system")
@app.get("/api/rvlink/system/wps_status_get", summary="??", response_model=Dict[str, Any], tags=["Status"])
async def get_wps_status_get():
return invoke_roof_unit_api(section_name="wps_status_get", query_method="GET", endpoint="system")
@app.get("/api/rvlink/system/workmode_config", summary="??", response_model=Dict[str, Any], tags=["Status"])
async def get_workmode_config():
return invoke_roof_unit_api(section_name="workmode_config", query_method="GET", endpoint="system")
@app.get("/api/rvlink/system/cpu_usage_and_flow", summary="??", response_model=Dict[str, Any], tags=["Status"])
async def get_cpu_usage_and_flow():
return invoke_roof_unit_api(section_name="cpu_usage_and_flow", query_method="GET", endpoint="system")
@app.get("/api/rvlink/system/first_login", summary="??", response_model=Dict[str, Any], tags=["Status"])
async def get_first_login():
return invoke_roof_unit_api(section_name="first_login", query_method="GET", endpoint="system")
@app.get("/api/rvlink/system/port_status", summary="??", response_model=Dict[str, Any], tags=["Status"])
async def get_port_status():
return invoke_roof_unit_api(section_name="port_status", query_method="GET", endpoint="system")
@app.get("/api/rvlink/system/ap_wtpd_status", summary="??", response_model=Dict[str, Any], tags=["Status"])
async def get_ap_wtpd_status():
return invoke_roof_unit_api(section_name="ap_wtpd_status", query_method="GET", endpoint="system")
@app.get("/api/rvlink/system/net_stats_get", summary="??", response_model=Dict[str, Any], tags=["Status"])
async def get_net_stats_get():
return invoke_roof_unit_api(section_name="net_stats_get", query_method="GET", endpoint="system")
# @app.get("/api/rvlink/system/permission_config", summary="??", response_model=Dict[str, Any], tags=["Configuration"])
# async def get_permission_config():
# return invoke_roof_unit_api(section_name="permission_config", query_method="SET", endpoint="system")
# --- Main Execution Block (for running with uvicorn directly) ---
if __name__ == "__main__":
print("Starting RVLink Direct API Proxy, by the Omnissiah's Will...")
uvicorn.run(
"rvlink_api:app", # Application import string
host="0.0.0.0",
port=8000,
log_level="info",
reload=True,
reload_excludes=["*.log", ".git/*", "__pycache__/*"], # Example: exclude certain files/dirs
reload_includes=["rvlink_api.py"] # Example: explicitly include only python files
)