279 lines
9.2 KiB
Python
279 lines
9.2 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, Request
|
|
from fastapi.responses import HTMLResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import func
|
|
from typing import List, Dict, Any
|
|
import logging
|
|
|
|
from app.slm.database import get_db as get_slm_db
|
|
from app.slm.models import NL43Config, NL43Status
|
|
from app.slm.services import NL43Client
|
|
# Import seismo database for roster data
|
|
from app.seismo.database import get_db as get_seismo_db
|
|
from app.seismo.models import RosterUnit
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/slm-dashboard", tags=["slm-dashboard"])
|
|
templates = Jinja2Templates(directory="app/ui/templates")
|
|
|
|
|
|
@router.get("/stats", response_class=HTMLResponse)
|
|
async def get_dashboard_stats(request: Request, db: Session = Depends(get_seismo_db)):
|
|
"""Get aggregate statistics for the SLM dashboard from roster (returns HTML)."""
|
|
# Query SLMs from the roster
|
|
slms = db.query(RosterUnit).filter_by(
|
|
device_type="sound_level_meter",
|
|
retired=False
|
|
).all()
|
|
|
|
total_units = len(slms)
|
|
deployed = sum(1 for s in slms if s.deployed)
|
|
benched = sum(1 for s in slms if not s.deployed)
|
|
|
|
# For "active", count SLMs with recent check-ins (within last hour)
|
|
from datetime import datetime, timedelta, timezone
|
|
one_hour_ago = datetime.now(timezone.utc) - timedelta(hours=1)
|
|
active = sum(1 for s in slms if s.slm_last_check and s.slm_last_check >= one_hour_ago)
|
|
|
|
# Map to template variable names
|
|
# total_count, deployed_count, active_count, benched_count
|
|
return templates.TemplateResponse(
|
|
"partials/slm_stats.html",
|
|
{
|
|
"request": request,
|
|
"total_count": total_units,
|
|
"deployed_count": deployed,
|
|
"active_count": active,
|
|
"benched_count": benched
|
|
}
|
|
)
|
|
|
|
|
|
@router.get("/units", response_class=HTMLResponse)
|
|
async def get_units_list(request: Request, db: Session = Depends(get_seismo_db)):
|
|
"""Get list of all SLM units from roster (returns HTML)."""
|
|
# Query SLMs from the roster (not retired)
|
|
slms = db.query(RosterUnit).filter_by(
|
|
device_type="sound_level_meter",
|
|
retired=False
|
|
).order_by(RosterUnit.id).all()
|
|
|
|
units = []
|
|
for slm in slms:
|
|
# Map to template field names
|
|
unit_data = {
|
|
"id": slm.id,
|
|
"slm_host": slm.slm_host,
|
|
"slm_tcp_port": slm.slm_tcp_port,
|
|
"slm_last_check": slm.slm_last_check,
|
|
"slm_model": slm.slm_model or "NL-43",
|
|
"address": slm.address,
|
|
"deployed_with_modem_id": slm.deployed_with_modem_id,
|
|
}
|
|
units.append(unit_data)
|
|
|
|
return templates.TemplateResponse(
|
|
"partials/slm_unit_list.html",
|
|
{
|
|
"request": request,
|
|
"units": units
|
|
}
|
|
)
|
|
|
|
|
|
@router.get("/live-view/{unit_id}", response_class=HTMLResponse)
|
|
async def get_live_view(unit_id: str, request: Request, slm_db: Session = Depends(get_slm_db), roster_db: Session = Depends(get_seismo_db)):
|
|
"""Get live measurement data for a specific unit (returns HTML)."""
|
|
# Get unit from roster
|
|
unit = roster_db.query(RosterUnit).filter_by(
|
|
id=unit_id,
|
|
device_type="sound_level_meter"
|
|
).first()
|
|
|
|
if not unit:
|
|
return templates.TemplateResponse(
|
|
"partials/slm_live_view_error.html",
|
|
{
|
|
"request": request,
|
|
"error": f"Unit {unit_id} not found in roster"
|
|
}
|
|
)
|
|
|
|
# Get status from monitoring database (may not exist yet)
|
|
status = slm_db.query(NL43Status).filter_by(unit_id=unit_id).first()
|
|
|
|
# Get modem info if available
|
|
modem = None
|
|
modem_ip = None
|
|
if unit.deployed_with_modem_id:
|
|
modem = roster_db.query(RosterUnit).filter_by(
|
|
id=unit.deployed_with_modem_id,
|
|
device_type="modem"
|
|
).first()
|
|
if modem:
|
|
modem_ip = modem.ip_address
|
|
elif unit.slm_host:
|
|
modem_ip = unit.slm_host
|
|
|
|
# Determine if measuring
|
|
is_measuring = False
|
|
if status and status.measurement_state:
|
|
is_measuring = status.measurement_state.lower() == 'start'
|
|
|
|
return templates.TemplateResponse(
|
|
"partials/slm_live_view.html",
|
|
{
|
|
"request": request,
|
|
"unit": unit,
|
|
"modem": modem,
|
|
"modem_ip": modem_ip,
|
|
"current_status": status,
|
|
"is_measuring": is_measuring
|
|
}
|
|
)
|
|
|
|
|
|
@router.get("/config/{unit_id}")
|
|
async def get_unit_config(unit_id: str, db: Session = Depends(get_slm_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_slm_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_slm_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_slm_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)
|
|
}
|