API built for most common commands

This commit is contained in:
serversdwn
2025-12-24 06:18:42 +00:00
parent 316cfa84f8
commit 60c95e825d
8 changed files with 1172 additions and 38 deletions

View File

@@ -258,6 +258,208 @@ async def manual_store(unit_id: str, db: Session = Depends(get_db)):
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/{unit_id}/pause")
async def pause_measurement(unit_id: str, db: Session = Depends(get_db)):
"""Pause the current measurement."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.pause()
logger.info(f"Paused measurement on unit {unit_id}")
return {"status": "ok", "message": "Measurement paused"}
except Exception as e:
logger.error(f"Failed to pause measurement on {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.post("/{unit_id}/resume")
async def resume_measurement(unit_id: str, db: Session = Depends(get_db)):
"""Resume a paused measurement."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.resume()
logger.info(f"Resumed measurement on unit {unit_id}")
return {"status": "ok", "message": "Measurement resumed"}
except Exception as e:
logger.error(f"Failed to resume measurement on {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.post("/{unit_id}/reset")
async def reset_measurement(unit_id: str, db: Session = Depends(get_db)):
"""Reset the measurement data."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.reset()
logger.info(f"Reset measurement data on unit {unit_id}")
return {"status": "ok", "message": "Measurement data reset"}
except Exception as e:
logger.error(f"Failed to reset measurement on {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/battery")
async def get_battery(unit_id: str, db: Session = Depends(get_db)):
"""Get battery level."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
level = await client.get_battery_level()
return {"status": "ok", "battery_level": level}
except Exception as e:
logger.error(f"Failed to get battery level for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/clock")
async def get_clock(unit_id: str, db: Session = Depends(get_db)):
"""Get device clock time."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
clock = await client.get_clock()
return {"status": "ok", "clock": clock}
except Exception as e:
logger.error(f"Failed to get clock for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
class ClockPayload(BaseModel):
datetime: str # Format: YYYY/MM/DD,HH:MM:SS
@router.put("/{unit_id}/clock")
async def set_clock(unit_id: str, payload: ClockPayload, db: Session = Depends(get_db)):
"""Set device clock time."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.set_clock(payload.datetime)
return {"status": "ok", "message": f"Clock set to {payload.datetime}"}
except Exception as e:
logger.error(f"Failed to set clock for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
class WeightingPayload(BaseModel):
weighting: str
channel: str = "Main"
@router.get("/{unit_id}/frequency-weighting")
async def get_frequency_weighting(unit_id: str, channel: str = "Main", db: Session = Depends(get_db)):
"""Get frequency weighting (A, C, Z)."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
weighting = await client.get_frequency_weighting(channel)
return {"status": "ok", "frequency_weighting": weighting, "channel": channel}
except Exception as e:
logger.error(f"Failed to get frequency weighting for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.put("/{unit_id}/frequency-weighting")
async def set_frequency_weighting(unit_id: str, payload: WeightingPayload, db: Session = Depends(get_db)):
"""Set frequency weighting (A, C, Z)."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.set_frequency_weighting(payload.weighting, payload.channel)
return {"status": "ok", "message": f"Frequency weighting set to {payload.weighting} on {payload.channel}"}
except Exception as e:
logger.error(f"Failed to set frequency weighting for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/time-weighting")
async def get_time_weighting(unit_id: str, channel: str = "Main", db: Session = Depends(get_db)):
"""Get time weighting (F, S, I)."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
weighting = await client.get_time_weighting(channel)
return {"status": "ok", "time_weighting": weighting, "channel": channel}
except Exception as e:
logger.error(f"Failed to get time weighting for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.put("/{unit_id}/time-weighting")
async def set_time_weighting(unit_id: str, payload: WeightingPayload, db: Session = Depends(get_db)):
"""Set time weighting (F, S, I)."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.set_time_weighting(payload.weighting, payload.channel)
return {"status": "ok", "message": f"Time weighting set to {payload.weighting} on {payload.channel}"}
except Exception as e:
logger.error(f"Failed to set time weighting for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/live")
async def live_status(unit_id: str, db: Session = Depends(get_db)):
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
@@ -292,6 +494,33 @@ async def live_status(unit_id: str, db: Session = Depends(get_db)):
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/{unit_id}/results")
async def get_results(unit_id: str, db: Session = Depends(get_db)):
"""Get final calculation results (DLC) from the last measurement."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
results = await client.request_dlc()
logger.info(f"Retrieved measurement results for unit {unit_id}")
return {"status": "ok", "data": results}
except ConnectionError as e:
logger.error(f"Failed to get results for {unit_id}: {e}")
raise HTTPException(status_code=502, detail="Failed to communicate with device")
except TimeoutError:
logger.error(f"Timeout getting results for {unit_id}")
raise HTTPException(status_code=504, detail="Device communication timeout")
except Exception as e:
logger.error(f"Unexpected error getting results for {unit_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.websocket("/{unit_id}/stream")
async def stream_live(websocket: WebSocket, unit_id: str):
"""WebSocket endpoint for real-time DRD streaming from NL43 device.

View File

@@ -13,7 +13,8 @@ from dataclasses import dataclass
from datetime import datetime
from typing import Optional, List
from sqlalchemy.orm import Session
import aioftp
from ftplib import FTP
from pathlib import Path
from app.models import NL43Status
@@ -237,6 +238,102 @@ class NL43Client:
await self._send_command("Manual Store,Start\r\n")
logger.info(f"Manual store executed on {self.device_key}")
async def pause(self):
"""Pause the current measurement."""
await self._send_command("Pause,On\r\n")
logger.info(f"Measurement paused on {self.device_key}")
async def resume(self):
"""Resume a paused measurement."""
await self._send_command("Pause,Off\r\n")
logger.info(f"Measurement resumed on {self.device_key}")
async def reset(self):
"""Reset the measurement data."""
await self._send_command("Reset\r\n")
logger.info(f"Measurement data reset on {self.device_key}")
async def get_battery_level(self) -> str:
"""Get the battery level."""
resp = await self._send_command("Battery Level?\r\n")
logger.info(f"Battery level on {self.device_key}: {resp}")
return resp.strip()
async def get_clock(self) -> str:
"""Get the device clock time."""
resp = await self._send_command("Clock?\r\n")
logger.info(f"Clock on {self.device_key}: {resp}")
return resp.strip()
async def set_clock(self, datetime_str: str):
"""Set the device clock time.
Args:
datetime_str: Time in format YYYY/MM/DD,HH:MM:SS
"""
await self._send_command(f"Clock,{datetime_str}\r\n")
logger.info(f"Clock set on {self.device_key} to {datetime_str}")
async def get_frequency_weighting(self, channel: str = "Main") -> str:
"""Get frequency weighting (A, C, Z, etc.).
Args:
channel: Main, Sub1, Sub2, or Sub3
"""
resp = await self._send_command(f"Frequency Weighting ({channel})?\r\n")
logger.info(f"Frequency weighting ({channel}) on {self.device_key}: {resp}")
return resp.strip()
async def set_frequency_weighting(self, weighting: str, channel: str = "Main"):
"""Set frequency weighting.
Args:
weighting: A, C, or Z
channel: Main, Sub1, Sub2, or Sub3
"""
await self._send_command(f"Frequency Weighting ({channel}),{weighting}\r\n")
logger.info(f"Frequency weighting ({channel}) set to {weighting} on {self.device_key}")
async def get_time_weighting(self, channel: str = "Main") -> str:
"""Get time weighting (F, S, I).
Args:
channel: Main, Sub1, Sub2, or Sub3
"""
resp = await self._send_command(f"Time Weighting ({channel})?\r\n")
logger.info(f"Time weighting ({channel}) on {self.device_key}: {resp}")
return resp.strip()
async def set_time_weighting(self, weighting: str, channel: str = "Main"):
"""Set time weighting.
Args:
weighting: F (Fast), S (Slow), or I (Impulse)
channel: Main, Sub1, Sub2, or Sub3
"""
await self._send_command(f"Time Weighting ({channel}),{weighting}\r\n")
logger.info(f"Time weighting ({channel}) set to {weighting} on {self.device_key}")
async def request_dlc(self) -> dict:
"""Request DLC (Data Last Calculation) - final stored measurement results.
This retrieves the complete calculation results from the last/current measurement,
including all statistical data. Similar to DOD but for final results.
Returns:
Dict with parsed DLC data
"""
resp = await self._send_command("DLC?\r\n")
logger.info(f"DLC data received from {self.device_key}: {resp[:100]}...")
# Parse DLC response - similar format to DOD
# The exact format depends on device configuration
# For now, return raw data - can be enhanced based on actual response format
return {
"raw_data": resp.strip(),
"device_key": self.device_key,
}
async def stream_drd(self, callback):
"""Stream continuous DRD output from the device.
@@ -380,23 +477,45 @@ class NL43Client:
"""
logger.info(f"Listing FTP files on {self.device_key} at {remote_path}")
try:
# FTP uses standard port 21, not the TCP control port
async with aioftp.Client.context(
self.host,
port=21,
user=self.ftp_username,
password=self.ftp_password,
socket_timeout=10
) as client:
def _list_ftp_sync():
"""Synchronous FTP listing using ftplib (supports active mode)."""
ftp = FTP()
ftp.set_debuglevel(0)
try:
# Connect and login
ftp.connect(self.host, 21, timeout=10)
ftp.login(self.ftp_username, self.ftp_password)
ftp.set_pasv(False) # Force active mode
# Change to target directory
if remote_path != "/":
ftp.cwd(remote_path)
# Get directory listing with details
files = []
async for path, info in client.list(remote_path):
lines = []
ftp.retrlines('LIST', lines.append)
for line in lines:
# Parse Unix-style ls output
parts = line.split(None, 8)
if len(parts) < 9:
continue
is_dir = parts[0].startswith('d')
size = int(parts[4]) if not is_dir else 0
name = parts[8]
# Skip . and ..
if name in ('.', '..'):
continue
file_info = {
"name": path.name,
"path": str(path),
"size": info.get("size", 0),
"modified": info.get("modify", ""),
"is_dir": info["type"] == "dir",
"name": name,
"path": f"{remote_path.rstrip('/')}/{name}",
"size": size,
"modified": f"{parts[5]} {parts[6]} {parts[7]}",
"is_dir": is_dir,
}
files.append(file_info)
logger.debug(f"Found file: {file_info}")
@@ -404,6 +523,15 @@ class NL43Client:
logger.info(f"Found {len(files)} files/directories on {self.device_key}")
return files
finally:
try:
ftp.quit()
except:
pass
try:
# Run synchronous FTP in thread pool
return await asyncio.to_thread(_list_ftp_sync)
except Exception as e:
logger.error(f"Failed to list FTP files on {self.device_key}: {e}")
raise ConnectionError(f"FTP connection failed: {str(e)}")
@@ -417,18 +545,31 @@ class NL43Client:
"""
logger.info(f"Downloading {remote_path} from {self.device_key} to {local_path}")
try:
# FTP uses standard port 21, not the TCP control port
async with aioftp.Client.context(
self.host,
port=21,
user=self.ftp_username,
password=self.ftp_password,
socket_timeout=10
) as client:
await client.download(remote_path, local_path, write_into=True)
def _download_ftp_sync():
"""Synchronous FTP download using ftplib (supports active mode)."""
ftp = FTP()
ftp.set_debuglevel(0)
try:
# Connect and login
ftp.connect(self.host, 21, timeout=10)
ftp.login(self.ftp_username, self.ftp_password)
ftp.set_pasv(False) # Force active mode
# Download file
with open(local_path, 'wb') as f:
ftp.retrbinary(f'RETR {remote_path}', f.write)
logger.info(f"Successfully downloaded {remote_path} to {local_path}")
finally:
try:
ftp.quit()
except:
pass
try:
# Run synchronous FTP in thread pool
await asyncio.to_thread(_download_ftp_sync)
except Exception as e:
logger.error(f"Failed to download {remote_path} from {self.device_key}: {e}")
raise ConnectionError(f"FTP download failed: {str(e)}")