""" Dashboard API endpoints for SLM/NL43 devices. This layer aggregates and transforms data from the device API for UI consumption. """ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from sqlalchemy import func from typing import List, Dict, Any import logging from app.slm.database import get_db from app.slm.models import NL43Config, NL43Status from app.slm.services import NL43Client logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/slm-dashboard", tags=["slm-dashboard"]) @router.get("/stats") async def get_dashboard_stats(db: Session = Depends(get_db)): """Get aggregate statistics for the SLM dashboard.""" total_units = db.query(func.count(NL43Config.unit_id)).scalar() or 0 # Count units with recent status updates (within last 5 minutes) from datetime import datetime, timedelta five_min_ago = datetime.utcnow() - timedelta(minutes=5) online_units = db.query(func.count(NL43Status.unit_id)).filter( NL43Status.last_seen >= five_min_ago ).scalar() or 0 # Count units currently measuring measuring_units = db.query(func.count(NL43Status.unit_id)).filter( NL43Status.measurement_state == "Measure" ).scalar() or 0 return { "total_units": total_units, "online_units": online_units, "offline_units": total_units - online_units, "measuring_units": measuring_units, "idle_units": online_units - measuring_units } @router.get("/units") async def get_units_list(db: Session = Depends(get_db)): """Get list of all NL43 units with their latest status.""" configs = db.query(NL43Config).all() units = [] for config in configs: status = db.query(NL43Status).filter_by(unit_id=config.unit_id).first() # Determine if unit is online (status updated within last 5 minutes) from datetime import datetime, timedelta is_online = False if status and status.last_seen: five_min_ago = datetime.utcnow() - timedelta(minutes=5) is_online = status.last_seen >= five_min_ago unit_data = { "unit_id": config.unit_id, "host": config.host, "tcp_port": config.tcp_port, "tcp_enabled": config.tcp_enabled, "is_online": is_online, "measurement_state": status.measurement_state if status else "unknown", "last_seen": status.last_seen.isoformat() if status and status.last_seen else None, "lp": status.lp if status else None, "leq": status.leq if status else None, "lmax": status.lmax if status else None, "battery_level": status.battery_level if status else None, } units.append(unit_data) return {"units": units} @router.get("/live-view/{unit_id}") async def get_live_view(unit_id: str, db: Session = Depends(get_db)): """Get live measurement data for a specific unit.""" status = db.query(NL43Status).filter_by(unit_id=unit_id).first() if not status: raise HTTPException(status_code=404, detail="Unit not found") return { "unit_id": unit_id, "last_seen": status.last_seen.isoformat() if status.last_seen else None, "measurement_state": status.measurement_state, "measurement_start_time": status.measurement_start_time.isoformat() if status.measurement_start_time else None, "counter": status.counter, "lp": status.lp, "leq": status.leq, "lmax": status.lmax, "lmin": status.lmin, "lpeak": status.lpeak, "battery_level": status.battery_level, "power_source": status.power_source, "sd_remaining_mb": status.sd_remaining_mb, "sd_free_ratio": status.sd_free_ratio, } @router.get("/config/{unit_id}") async def get_unit_config(unit_id: str, db: Session = Depends(get_db)): """Get configuration for a specific unit.""" config = db.query(NL43Config).filter_by(unit_id=unit_id).first() if not config: raise HTTPException(status_code=404, detail="Unit configuration not found") return { "unit_id": config.unit_id, "host": config.host, "tcp_port": config.tcp_port, "tcp_enabled": config.tcp_enabled, "ftp_enabled": config.ftp_enabled, "ftp_username": config.ftp_username, "ftp_password": config.ftp_password, "web_enabled": config.web_enabled, } @router.post("/config/{unit_id}") async def update_unit_config(unit_id: str, config_data: Dict[str, Any], db: Session = Depends(get_db)): """Update configuration for a specific unit.""" config = db.query(NL43Config).filter_by(unit_id=unit_id).first() if not config: # Create new config config = NL43Config(unit_id=unit_id) db.add(config) # Update fields if "host" in config_data: config.host = config_data["host"] if "tcp_port" in config_data: config.tcp_port = config_data["tcp_port"] if "tcp_enabled" in config_data: config.tcp_enabled = config_data["tcp_enabled"] if "ftp_enabled" in config_data: config.ftp_enabled = config_data["ftp_enabled"] if "ftp_username" in config_data: config.ftp_username = config_data["ftp_username"] if "ftp_password" in config_data: config.ftp_password = config_data["ftp_password"] if "web_enabled" in config_data: config.web_enabled = config_data["web_enabled"] db.commit() db.refresh(config) return {"success": True, "unit_id": unit_id} @router.post("/control/{unit_id}/{action}") async def control_unit(unit_id: str, action: str, db: Session = Depends(get_db)): """Send control command to a unit (start, stop, pause, resume, etc.).""" config = db.query(NL43Config).filter_by(unit_id=unit_id).first() if not config: raise HTTPException(status_code=404, detail="Unit configuration not found") if not config.tcp_enabled: raise HTTPException(status_code=400, detail="TCP control not enabled for this unit") # Create NL43Client client = NL43Client( host=config.host, port=config.tcp_port, timeout=5.0, ftp_username=config.ftp_username, ftp_password=config.ftp_password ) # Map action to command action_map = { "start": "start_measurement", "stop": "stop_measurement", "pause": "pause_measurement", "resume": "resume_measurement", "reset": "reset_measurement", "sleep": "sleep_mode", "wake": "wake_from_sleep", } if action not in action_map: raise HTTPException(status_code=400, detail=f"Unknown action: {action}") method_name = action_map[action] method = getattr(client, method_name, None) if not method: raise HTTPException(status_code=500, detail=f"Method {method_name} not implemented") try: result = await method() return {"success": True, "action": action, "result": result} except Exception as e: logger.error(f"Error executing {action} on {unit_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/test-modem/{unit_id}") async def test_modem(unit_id: str, db: Session = Depends(get_db)): """Test connectivity to a unit's modem/device.""" config = db.query(NL43Config).filter_by(unit_id=unit_id).first() if not config: raise HTTPException(status_code=404, detail="Unit configuration not found") if not config.tcp_enabled: raise HTTPException(status_code=400, detail="TCP control not enabled for this unit") client = NL43Client( host=config.host, port=config.tcp_port, timeout=5.0, ftp_username=config.ftp_username, ftp_password=config.ftp_password ) try: # Try to get measurement state as a connectivity test state = await client.get_measurement_state() return { "success": True, "unit_id": unit_id, "host": config.host, "port": config.tcp_port, "reachable": True, "measurement_state": state } except Exception as e: logger.warning(f"Modem test failed for {unit_id}: {e}") return { "success": False, "unit_id": unit_id, "host": config.host, "port": config.tcp_port, "reachable": False, "error": str(e) }