SLM config now sync to SLMM, SLMM caches configs for speed

This commit is contained in:
serversdwn
2026-01-07 18:33:58 +00:00
parent 6d34e543fe
commit c30d7fac22
12 changed files with 1893 additions and 124 deletions

View File

@@ -1,13 +1,21 @@
from fastapi import APIRouter, Depends, HTTPException, Form, UploadFile, File
from fastapi import APIRouter, Depends, HTTPException, Form, UploadFile, File, Request
from fastapi.exceptions import RequestValidationError
from sqlalchemy.orm import Session
from datetime import datetime, date
import csv
import io
import logging
import httpx
import os
from backend.database import get_db
from backend.models import RosterUnit, IgnoredUnit, Emitter, UnitHistory
router = APIRouter(prefix="/api/roster", tags=["roster-edit"])
logger = logging.getLogger(__name__)
# SLMM backend URL for syncing device configs to cache
SLMM_BASE_URL = os.getenv("SLMM_BASE_URL", "http://localhost:8100")
def record_history(db: Session, unit_id: str, change_type: str, field_name: str = None,
@@ -37,13 +45,98 @@ def get_or_create_roster_unit(db: Session, unit_id: str):
return unit
async def sync_slm_to_slmm_cache(
unit_id: str,
host: str = None,
tcp_port: int = None,
ftp_port: int = None,
ftp_username: str = None,
ftp_password: str = None,
deployed_with_modem_id: str = None,
db: Session = None
) -> dict:
"""
Sync SLM device configuration to SLMM backend cache.
Terra-View is the source of truth for device configs. This function updates
SLMM's config cache (NL43Config table) so SLMM can look up device connection
info by unit_id without Terra-View passing host:port with every request.
Args:
unit_id: Unique identifier for the SLM device
host: Direct IP address/hostname OR will be resolved from modem
tcp_port: TCP control port (default: 2255)
ftp_port: FTP port (default: 21)
ftp_username: FTP username (optional)
ftp_password: FTP password (optional)
deployed_with_modem_id: If set, resolve modem IP as host
db: Database session for modem lookup
Returns:
dict: {"success": bool, "message": str}
"""
# Resolve host from modem if assigned
if deployed_with_modem_id and db:
modem = db.query(RosterUnit).filter_by(
id=deployed_with_modem_id,
device_type="modem"
).first()
if modem and modem.ip_address:
host = modem.ip_address
logger.info(f"Resolved host from modem {deployed_with_modem_id}: {host}")
# Validate required fields
if not host:
logger.warning(f"Cannot sync SLM {unit_id} to SLMM: no host/IP address provided")
return {"success": False, "message": "No host IP address available"}
# Set defaults
tcp_port = tcp_port or 2255
ftp_port = ftp_port or 21
# Build SLMM cache payload
config_payload = {
"host": host,
"tcp_port": tcp_port,
"tcp_enabled": True,
"ftp_enabled": bool(ftp_username and ftp_password),
"web_enabled": False
}
if ftp_username and ftp_password:
config_payload["ftp_username"] = ftp_username
config_payload["ftp_password"] = ftp_password
# Call SLMM cache update API
slmm_url = f"{SLMM_BASE_URL}/api/nl43/{unit_id}/config"
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.put(slmm_url, json=config_payload)
if response.status_code in [200, 201]:
logger.info(f"Successfully synced SLM {unit_id} to SLMM cache")
return {"success": True, "message": "Device config cached in SLMM"}
else:
logger.error(f"SLMM cache sync failed for {unit_id}: HTTP {response.status_code}")
return {"success": False, "message": f"SLMM returned status {response.status_code}"}
except httpx.ConnectError:
logger.error(f"Cannot connect to SLMM service at {SLMM_BASE_URL}")
return {"success": False, "message": "SLMM service unavailable"}
except Exception as e:
logger.error(f"Error syncing SLM {unit_id} to SLMM: {e}")
return {"success": False, "message": str(e)}
@router.post("/add")
def add_roster_unit(
async def add_roster_unit(
id: str = Form(...),
device_type: str = Form("seismograph"),
unit_type: str = Form("series3"),
deployed: bool = Form(False),
retired: bool = Form(False),
deployed: str = Form(None),
retired: str = Form(None),
note: str = Form(""),
project_id: str = Form(None),
location: str = Form(None),
@@ -68,9 +161,11 @@ def add_roster_unit(
slm_measurement_range: str = Form(None),
db: Session = Depends(get_db)
):
import logging
logger = logging.getLogger(__name__)
logger.info(f"Adding unit: id={id}, device_type={device_type}, slm_tcp_port={slm_tcp_port}, slm_ftp_port={slm_ftp_port}")
logger.info(f"Adding unit: id={id}, device_type={device_type}, deployed={deployed}, retired={retired}")
# Convert boolean strings to actual booleans
deployed_bool = deployed in ['true', 'True', '1', 'yes'] if deployed else False
retired_bool = retired in ['true', 'True', '1', 'yes'] if retired else False
# Convert port strings to integers
slm_tcp_port_int = int(slm_tcp_port) if slm_tcp_port and slm_tcp_port.strip() else None
@@ -98,8 +193,8 @@ def add_roster_unit(
id=id,
device_type=device_type,
unit_type=unit_type,
deployed=deployed,
retired=retired,
deployed=deployed_bool,
retired=retired_bool,
note=note,
project_id=project_id,
location=location,
@@ -126,6 +221,24 @@ def add_roster_unit(
)
db.add(unit)
db.commit()
# If sound level meter, sync config to SLMM cache
if device_type == "sound_level_meter":
logger.info(f"Syncing SLM {id} config to SLMM cache...")
result = await sync_slm_to_slmm_cache(
unit_id=id,
host=slm_host,
tcp_port=slm_tcp_port_int,
ftp_port=slm_ftp_port_int,
deployed_with_modem_id=deployed_with_modem_id,
db=db
)
if not result["success"]:
logger.warning(f"SLMM cache sync warning for {id}: {result['message']}")
# Don't fail the operation - device is still added to Terra-View roster
# User can manually sync later or SLMM will be synced on next config update
return {"message": "Unit added", "id": id, "device_type": device_type}
@@ -186,8 +299,8 @@ def edit_roster_unit(
unit_id: str,
device_type: str = Form("seismograph"),
unit_type: str = Form("series3"),
deployed: bool = Form(False),
retired: bool = Form(False),
deployed: str = Form(None),
retired: str = Form(None),
note: str = Form(""),
project_id: str = Form(None),
location: str = Form(None),
@@ -216,6 +329,10 @@ def edit_roster_unit(
if not unit:
raise HTTPException(status_code=404, detail="Unit not found")
# Convert boolean strings to actual booleans
deployed_bool = deployed in ['true', 'True', '1', 'yes'] if deployed else False
retired_bool = retired in ['true', 'True', '1', 'yes'] if retired else False
# Convert port strings to integers
slm_tcp_port_int = int(slm_tcp_port) if slm_tcp_port and slm_tcp_port.strip() else None
slm_ftp_port_int = int(slm_ftp_port) if slm_ftp_port and slm_ftp_port.strip() else None
@@ -243,8 +360,8 @@ def edit_roster_unit(
# Update all fields
unit.device_type = device_type
unit.unit_type = unit_type
unit.deployed = deployed
unit.retired = retired
unit.deployed = deployed_bool
unit.retired = retired_bool
unit.note = note
unit.project_id = project_id
unit.location = location