242 lines
8.3 KiB
Python
242 lines
8.3 KiB
Python
"""
|
|
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)
|
|
}
|