Add FTP credentials management and UI enhancements
- Implement migration script to add ftp_username and ftp_password columns to nl43_config table. - Create set_ftp_credentials.py script for updating FTP credentials in the database. - Update requirements.txt to include aioftp for FTP functionality. - Enhance index.html with FTP controls including enable, disable, check status, and list files features. - Add JavaScript functions for handling FTP operations and displaying file lists.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/manuals/
|
||||
/data/
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -73,7 +73,7 @@ async def health_devices():
|
||||
configs = db.query(NL43Config).filter_by(tcp_enabled=True).all()
|
||||
|
||||
for cfg in configs:
|
||||
client = NL43Client(cfg.host, cfg.tcp_port, timeout=2.0)
|
||||
client = NL43Client(cfg.host, cfg.tcp_port, timeout=2.0, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
|
||||
status = {
|
||||
"unit_id": cfg.unit_id,
|
||||
"host": cfg.host,
|
||||
|
||||
@@ -14,6 +14,8 @@ class NL43Config(Base):
|
||||
tcp_port = Column(Integer, default=80) # NL43 TCP control port (via RX55)
|
||||
tcp_enabled = Column(Boolean, default=True)
|
||||
ftp_enabled = Column(Boolean, default=False)
|
||||
ftp_username = Column(String, nullable=True) # FTP login username
|
||||
ftp_password = Column(String, nullable=True) # FTP login password
|
||||
web_enabled = Column(Boolean, default=False)
|
||||
|
||||
|
||||
|
||||
255
app/routers.py
255
app/routers.py
@@ -1,9 +1,12 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, field_validator
|
||||
import logging
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import NL43Config, NL43Status
|
||||
@@ -19,6 +22,8 @@ class ConfigPayload(BaseModel):
|
||||
tcp_port: int | None = None
|
||||
tcp_enabled: bool | None = None
|
||||
ftp_enabled: bool | None = None
|
||||
ftp_username: str | None = None
|
||||
ftp_password: str | None = None
|
||||
web_enabled: bool | None = None
|
||||
|
||||
@field_validator("host")
|
||||
@@ -81,6 +86,10 @@ def upsert_config(unit_id: str, payload: ConfigPayload, db: Session = Depends(ge
|
||||
cfg.tcp_enabled = payload.tcp_enabled
|
||||
if payload.ftp_enabled is not None:
|
||||
cfg.ftp_enabled = payload.ftp_enabled
|
||||
if payload.ftp_username is not None:
|
||||
cfg.ftp_username = payload.ftp_username
|
||||
if payload.ftp_password is not None:
|
||||
cfg.ftp_password = payload.ftp_password
|
||||
if payload.web_enabled is not None:
|
||||
cfg.web_enabled = payload.web_enabled
|
||||
|
||||
@@ -182,7 +191,7 @@ async def start_measurement(unit_id: str, db: Session = Depends(get_db)):
|
||||
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)
|
||||
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
|
||||
try:
|
||||
await client.start()
|
||||
logger.info(f"Started measurement on unit {unit_id}")
|
||||
@@ -207,7 +216,7 @@ async def stop_measurement(unit_id: str, db: Session = Depends(get_db)):
|
||||
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)
|
||||
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
|
||||
try:
|
||||
await client.stop()
|
||||
logger.info(f"Stopped measurement on unit {unit_id}")
|
||||
@@ -223,6 +232,32 @@ async def stop_measurement(unit_id: str, db: Session = Depends(get_db)):
|
||||
return {"status": "ok", "message": "Measurement stopped"}
|
||||
|
||||
|
||||
@router.post("/{unit_id}/store")
|
||||
async def manual_store(unit_id: str, db: Session = Depends(get_db)):
|
||||
"""Manually store measurement data to SD card."""
|
||||
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.manual_store()
|
||||
logger.info(f"Manual store executed on unit {unit_id}")
|
||||
return {"status": "ok", "message": "Data stored to SD card"}
|
||||
except ConnectionError as e:
|
||||
logger.error(f"Failed to store data on {unit_id}: {e}")
|
||||
raise HTTPException(status_code=502, detail="Failed to communicate with device")
|
||||
except TimeoutError:
|
||||
logger.error(f"Timeout storing data on {unit_id}")
|
||||
raise HTTPException(status_code=504, detail="Device communication timeout")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error storing data on {unit_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@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()
|
||||
@@ -232,7 +267,7 @@ async def live_status(unit_id: str, db: Session = Depends(get_db)):
|
||||
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)
|
||||
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
|
||||
try:
|
||||
snap = await client.request_dod()
|
||||
snap.unit_id = unit_id
|
||||
@@ -255,3 +290,215 @@ async def live_status(unit_id: str, db: Session = Depends(get_db)):
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error getting live status 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.
|
||||
|
||||
Connects to the device, starts DRD streaming, and pushes updates to the WebSocket client.
|
||||
The stream continues until the client disconnects or an error occurs.
|
||||
"""
|
||||
await websocket.accept()
|
||||
logger.info(f"WebSocket connection accepted for unit {unit_id}")
|
||||
|
||||
from app.database import SessionLocal
|
||||
|
||||
db: Session = SessionLocal()
|
||||
|
||||
try:
|
||||
# Get device configuration
|
||||
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
|
||||
if not cfg:
|
||||
await websocket.send_json({"error": "NL43 config not found", "unit_id": unit_id})
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
if not cfg.tcp_enabled:
|
||||
await websocket.send_json(
|
||||
{"error": "TCP communication is disabled for this device", "unit_id": unit_id}
|
||||
)
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
# Create client and define callback
|
||||
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
|
||||
|
||||
async def send_snapshot(snap):
|
||||
"""Callback that sends each snapshot to the WebSocket client."""
|
||||
snap.unit_id = unit_id
|
||||
|
||||
# Persist to database
|
||||
try:
|
||||
persist_snapshot(snap, db)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to persist snapshot during stream: {e}")
|
||||
|
||||
# Send to WebSocket client
|
||||
try:
|
||||
await websocket.send_json({
|
||||
"unit_id": unit_id,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"measurement_state": snap.measurement_state,
|
||||
"lp": snap.lp,
|
||||
"leq": snap.leq,
|
||||
"lmax": snap.lmax,
|
||||
"lmin": snap.lmin,
|
||||
"lpeak": snap.lpeak,
|
||||
"raw_payload": snap.raw_payload,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send snapshot via WebSocket: {e}")
|
||||
raise
|
||||
|
||||
# Start DRD streaming
|
||||
logger.info(f"Starting DRD stream for unit {unit_id}")
|
||||
await client.stream_drd(send_snapshot)
|
||||
|
||||
except WebSocketDisconnect:
|
||||
logger.info(f"WebSocket disconnected for unit {unit_id}")
|
||||
except ConnectionError as e:
|
||||
logger.error(f"Failed to connect to device {unit_id}: {e}")
|
||||
try:
|
||||
await websocket.send_json({"error": "Failed to communicate with device", "detail": str(e)})
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in WebSocket stream for {unit_id}: {e}")
|
||||
try:
|
||||
await websocket.send_json({"error": "Internal server error", "detail": str(e)})
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
db.close()
|
||||
try:
|
||||
await websocket.close()
|
||||
except Exception:
|
||||
pass
|
||||
logger.info(f"WebSocket stream closed for unit {unit_id}")
|
||||
|
||||
|
||||
@router.post("/{unit_id}/ftp/enable")
|
||||
async def enable_ftp(unit_id: str, db: Session = Depends(get_db)):
|
||||
"""Enable FTP server on the device."""
|
||||
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.enable_ftp()
|
||||
logger.info(f"Enabled FTP on unit {unit_id}")
|
||||
return {"status": "ok", "message": "FTP enabled"}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to enable FTP on {unit_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to enable FTP: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/{unit_id}/ftp/disable")
|
||||
async def disable_ftp(unit_id: str, db: Session = Depends(get_db)):
|
||||
"""Disable FTP server on the device."""
|
||||
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.disable_ftp()
|
||||
logger.info(f"Disabled FTP on unit {unit_id}")
|
||||
return {"status": "ok", "message": "FTP disabled"}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to disable FTP on {unit_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to disable FTP: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{unit_id}/ftp/status")
|
||||
async def get_ftp_status(unit_id: str, db: Session = Depends(get_db)):
|
||||
"""Get FTP server status from the device."""
|
||||
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:
|
||||
status = await client.get_ftp_status()
|
||||
return {"status": "ok", "ftp_enabled": status.lower() == "on", "ftp_status": status}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get FTP status from {unit_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get FTP status: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{unit_id}/ftp/files")
|
||||
async def list_ftp_files(unit_id: str, path: str = "/", db: Session = Depends(get_db)):
|
||||
"""List files on the device via FTP.
|
||||
|
||||
Query params:
|
||||
path: Directory path on the device (default: root)
|
||||
"""
|
||||
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
|
||||
if not cfg:
|
||||
raise HTTPException(status_code=404, detail="NL43 config not found")
|
||||
|
||||
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
|
||||
try:
|
||||
files = await client.list_ftp_files(path)
|
||||
return {"status": "ok", "path": path, "files": files, "count": len(files)}
|
||||
except ConnectionError as e:
|
||||
logger.error(f"Failed to list FTP files on {unit_id}: {e}")
|
||||
raise HTTPException(status_code=502, detail="Failed to communicate with device")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error listing FTP files on {unit_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
class DownloadRequest(BaseModel):
|
||||
remote_path: str
|
||||
|
||||
|
||||
@router.post("/{unit_id}/ftp/download")
|
||||
async def download_ftp_file(unit_id: str, payload: DownloadRequest, db: Session = Depends(get_db)):
|
||||
"""Download a file from the device via FTP.
|
||||
|
||||
The file is saved to data/downloads/{unit_id}/ and can be retrieved via the response.
|
||||
"""
|
||||
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
|
||||
if not cfg:
|
||||
raise HTTPException(status_code=404, detail="NL43 config not found")
|
||||
|
||||
# Create download directory
|
||||
download_dir = f"data/downloads/{unit_id}"
|
||||
os.makedirs(download_dir, exist_ok=True)
|
||||
|
||||
# Extract filename from remote path
|
||||
filename = os.path.basename(payload.remote_path)
|
||||
if not filename:
|
||||
raise HTTPException(status_code=400, detail="Invalid remote path")
|
||||
|
||||
local_path = os.path.join(download_dir, filename)
|
||||
|
||||
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
|
||||
try:
|
||||
await client.download_ftp_file(payload.remote_path, local_path)
|
||||
logger.info(f"Downloaded {payload.remote_path} from {unit_id} to {local_path}")
|
||||
|
||||
# Return the file
|
||||
return FileResponse(
|
||||
path=local_path,
|
||||
filename=filename,
|
||||
media_type="application/octet-stream",
|
||||
)
|
||||
except ConnectionError as e:
|
||||
logger.error(f"Failed to download file from {unit_id}: {e}")
|
||||
raise HTTPException(status_code=502, detail="Failed to communicate with device")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error downloading file from {unit_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
221
app/services.py
221
app/services.py
@@ -11,8 +11,9 @@ import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
from sqlalchemy.orm import Session
|
||||
import aioftp
|
||||
|
||||
from app.models import NL43Status
|
||||
|
||||
@@ -69,10 +70,12 @@ _rate_limit_lock = asyncio.Lock()
|
||||
|
||||
|
||||
class NL43Client:
|
||||
def __init__(self, host: str, port: int, timeout: float = 5.0):
|
||||
def __init__(self, host: str, port: int, timeout: float = 5.0, ftp_username: str = None, ftp_password: str = None):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
self.ftp_username = ftp_username or "anonymous"
|
||||
self.ftp_password = ftp_password or ""
|
||||
self.device_key = f"{host}:{port}"
|
||||
|
||||
async def _enforce_rate_limit(self):
|
||||
@@ -215,3 +218,217 @@ class NL43Client:
|
||||
According to NL43 protocol: Measure,Stop (no $ prefix, capitalized param)
|
||||
"""
|
||||
await self._send_command("Measure,Stop\r\n")
|
||||
|
||||
async def set_store_mode_manual(self):
|
||||
"""Set the device to Manual Store mode.
|
||||
|
||||
According to NL43 protocol: Store Mode,Manual sets manual storage mode
|
||||
"""
|
||||
await self._send_command("Store Mode,Manual\r\n")
|
||||
logger.info(f"Store mode set to Manual on {self.device_key}")
|
||||
|
||||
async def manual_store(self):
|
||||
"""Manually store the current measurement data.
|
||||
|
||||
According to NL43 protocol: Manual Store,Start executes storing
|
||||
Parameter p1="Start" executes the storage operation
|
||||
Device must be in Manual Store mode first
|
||||
"""
|
||||
await self._send_command("Manual Store,Start\r\n")
|
||||
logger.info(f"Manual store executed on {self.device_key}")
|
||||
|
||||
async def stream_drd(self, callback):
|
||||
"""Stream continuous DRD output from the device.
|
||||
|
||||
Opens a persistent connection and streams DRD data lines.
|
||||
Calls the provided callback function with each parsed snapshot.
|
||||
|
||||
Args:
|
||||
callback: Async function that receives NL43Snapshot objects
|
||||
|
||||
The stream continues until an exception occurs or the connection is closed.
|
||||
Send SUB character (0x1A) to stop the stream.
|
||||
"""
|
||||
await self._enforce_rate_limit()
|
||||
|
||||
logger.info(f"Starting DRD stream for {self.device_key}")
|
||||
|
||||
try:
|
||||
reader, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(self.host, self.port), timeout=self.timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"DRD stream connection timeout to {self.device_key}")
|
||||
raise ConnectionError(f"Failed to connect to device at {self.host}:{self.port}")
|
||||
except Exception as e:
|
||||
logger.error(f"DRD stream connection failed to {self.device_key}: {e}")
|
||||
raise ConnectionError(f"Failed to connect to device: {str(e)}")
|
||||
|
||||
try:
|
||||
# Start DRD streaming
|
||||
writer.write(b"DRD?\r\n")
|
||||
await writer.drain()
|
||||
|
||||
# Read initial result code
|
||||
first_line_data = await asyncio.wait_for(reader.readuntil(b"\n"), timeout=self.timeout)
|
||||
result_code = first_line_data.decode(errors="ignore").strip()
|
||||
|
||||
if result_code.startswith("$"):
|
||||
result_code = result_code[1:].strip()
|
||||
|
||||
logger.debug(f"DRD stream result code from {self.device_key}: {result_code}")
|
||||
|
||||
if result_code != "R+0000":
|
||||
raise ValueError(f"DRD stream failed to start: {result_code}")
|
||||
|
||||
logger.info(f"DRD stream started successfully for {self.device_key}")
|
||||
|
||||
# Continuously read data lines
|
||||
while True:
|
||||
try:
|
||||
line_data = await asyncio.wait_for(reader.readuntil(b"\n"), timeout=30.0)
|
||||
line = line_data.decode(errors="ignore").strip()
|
||||
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Remove leading $ if present
|
||||
if line.startswith("$"):
|
||||
line = line[1:].strip()
|
||||
|
||||
# Parse the DRD data (same format as DOD)
|
||||
parts = [p.strip() for p in line.split(",") if p.strip() != ""]
|
||||
|
||||
if len(parts) < 2:
|
||||
logger.warning(f"Malformed DRD data from {self.device_key}: {line}")
|
||||
continue
|
||||
|
||||
snap = NL43Snapshot(unit_id="", raw_payload=line, measurement_state="Measure")
|
||||
|
||||
# Parse known positions
|
||||
try:
|
||||
if len(parts) >= 1:
|
||||
snap.lp = parts[0]
|
||||
if len(parts) >= 2:
|
||||
snap.leq = parts[1]
|
||||
if len(parts) >= 4:
|
||||
snap.lmax = parts[3]
|
||||
if len(parts) >= 5:
|
||||
snap.lmin = parts[4]
|
||||
if len(parts) >= 11:
|
||||
snap.lpeak = parts[10]
|
||||
except (IndexError, ValueError) as e:
|
||||
logger.warning(f"Error parsing DRD data points: {e}")
|
||||
|
||||
# Call the callback with the snapshot
|
||||
await callback(snap)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"DRD stream timeout (no data for 30s) from {self.device_key}")
|
||||
break
|
||||
except asyncio.IncompleteReadError:
|
||||
logger.info(f"DRD stream closed by device {self.device_key}")
|
||||
break
|
||||
|
||||
finally:
|
||||
# Send SUB character to stop streaming
|
||||
try:
|
||||
writer.write(b"\x1A")
|
||||
await writer.drain()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
writer.close()
|
||||
with contextlib.suppress(Exception):
|
||||
await writer.wait_closed()
|
||||
|
||||
logger.info(f"DRD stream ended for {self.device_key}")
|
||||
|
||||
async def enable_ftp(self):
|
||||
"""Enable FTP server on the device.
|
||||
|
||||
According to NL43 protocol: FTP,On enables the FTP server
|
||||
"""
|
||||
await self._send_command("FTP,On\r\n")
|
||||
logger.info(f"FTP enabled on {self.device_key}")
|
||||
|
||||
async def disable_ftp(self):
|
||||
"""Disable FTP server on the device.
|
||||
|
||||
According to NL43 protocol: FTP,Off disables the FTP server
|
||||
"""
|
||||
await self._send_command("FTP,Off\r\n")
|
||||
logger.info(f"FTP disabled on {self.device_key}")
|
||||
|
||||
async def get_ftp_status(self) -> str:
|
||||
"""Query FTP server status on the device.
|
||||
|
||||
Returns: "On" or "Off"
|
||||
"""
|
||||
resp = await self._send_command("FTP?\r\n")
|
||||
logger.info(f"FTP status on {self.device_key}: {resp}")
|
||||
return resp.strip()
|
||||
|
||||
async def list_ftp_files(self, remote_path: str = "/") -> List[dict]:
|
||||
"""List files on the device via FTP.
|
||||
|
||||
Args:
|
||||
remote_path: Directory path on the device (default: root)
|
||||
|
||||
Returns:
|
||||
List of file info dicts with 'name', 'size', 'modified', 'is_dir'
|
||||
"""
|
||||
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:
|
||||
files = []
|
||||
async for path, info in client.list(remote_path):
|
||||
file_info = {
|
||||
"name": path.name,
|
||||
"path": str(path),
|
||||
"size": info.get("size", 0),
|
||||
"modified": info.get("modify", ""),
|
||||
"is_dir": info["type"] == "dir",
|
||||
}
|
||||
files.append(file_info)
|
||||
logger.debug(f"Found file: {file_info}")
|
||||
|
||||
logger.info(f"Found {len(files)} files/directories on {self.device_key}")
|
||||
return files
|
||||
|
||||
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)}")
|
||||
|
||||
async def download_ftp_file(self, remote_path: str, local_path: str):
|
||||
"""Download a file from the device via FTP.
|
||||
|
||||
Args:
|
||||
remote_path: Full path to file on the device
|
||||
local_path: Local path where file will be saved
|
||||
"""
|
||||
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)
|
||||
logger.info(f"Successfully downloaded {remote_path} to {local_path}")
|
||||
|
||||
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)}")
|
||||
|
||||
BIN
data/slmm.db
BIN
data/slmm.db
Binary file not shown.
288
data/slmm.log
288
data/slmm.log
@@ -91,3 +91,291 @@
|
||||
2025-12-23 20:29:57,135 - app.routers - INFO - Started measurement on unit nl43-1
|
||||
2025-12-23 20:30:46,229 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Stop
|
||||
2025-12-23 20:30:46,455 - app.routers - INFO - Stopped measurement on unit nl43-1
|
||||
2025-12-23 20:35:33,744 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 20:35:33,993 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 20:35:34,000 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 20:35:57,872 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 20:36:02,874 - app.services - ERROR - Connection timeout to 63.45.161.30:2255
|
||||
2025-12-23 20:36:02,874 - app.routers - ERROR - Failed to get live status for nl43-1: Failed to connect to device at 63.45.161.30:2255
|
||||
2025-12-23 20:37:56,046 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 20:37:56,234 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 20:37:56,254 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 20:38:02,637 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Start
|
||||
2025-12-23 20:38:02,774 - app.routers - INFO - Started measurement on unit nl43-1
|
||||
2025-12-23 20:38:09,492 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 20:38:09,665 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 20:38:09,681 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 21:14:01,816 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Stop
|
||||
2025-12-23 21:14:02,056 - app.routers - INFO - Stopped measurement on unit nl43-1
|
||||
2025-12-23 21:14:15,196 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 21:14:15,346 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 21:14:15,357 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 21:44:51,888 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 21:44:51,888 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 21:45:29,201 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 21:45:29,201 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 21:45:46,360 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 21:45:46,360 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 22:18:54,486 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 22:18:54,657 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 22:18:54,667 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 22:21:29,945 - app.routers - INFO - WebSocket connection accepted for unit nl43-1
|
||||
2025-12-23 22:21:29,945 - app.routers - INFO - Starting DRD stream for unit nl43-1
|
||||
2025-12-23 22:21:29,945 - app.services - INFO - Starting DRD stream for 63.45.161.30:2255
|
||||
2025-12-23 22:21:30,159 - app.services - INFO - DRD stream started successfully for 63.45.161.30:2255
|
||||
2025-12-23 22:22:08,492 - app.routers - ERROR - Failed to send snapshot via WebSocket: received 1005 (no status received [internal]); then sent 1005 (no status received [internal])
|
||||
2025-12-23 22:22:08,492 - app.services - INFO - DRD stream ended for 63.45.161.30:2255
|
||||
2025-12-23 22:22:08,492 - app.routers - ERROR - Unexpected error in WebSocket stream for nl43-1: received 1005 (no status received [internal]); then sent 1005 (no status received [internal])
|
||||
2025-12-23 22:22:08,492 - app.routers - INFO - WebSocket stream closed for unit nl43-1
|
||||
2025-12-23 23:04:34,743 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 23:04:34,743 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 23:04:40,114 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On
|
||||
2025-12-23 23:04:40,280 - app.services - INFO - FTP enabled on 63.45.161.30:2255
|
||||
2025-12-23 23:04:40,280 - app.routers - INFO - Enabled FTP on unit nl43-1
|
||||
2025-12-23 23:04:46,040 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On
|
||||
2025-12-23 23:04:46,200 - app.services - INFO - FTP enabled on 63.45.161.30:2255
|
||||
2025-12-23 23:04:46,200 - app.routers - INFO - Enabled FTP on unit nl43-1
|
||||
2025-12-23 23:04:48,486 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP?
|
||||
2025-12-23 23:04:48,639 - app.services - INFO - FTP status on 63.45.161.30:2255: On
|
||||
2025-12-23 23:04:59,824 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On
|
||||
2025-12-23 23:05:00,080 - app.services - INFO - FTP enabled on 63.45.161.30:2255
|
||||
2025-12-23 23:05:00,080 - app.routers - INFO - Enabled FTP on unit nl43-1
|
||||
2025-12-23 23:05:07,794 - app.routers - INFO - WebSocket connection accepted for unit nl43-1
|
||||
2025-12-23 23:05:07,795 - app.routers - INFO - Starting DRD stream for unit nl43-1
|
||||
2025-12-23 23:05:07,795 - app.services - INFO - Starting DRD stream for 63.45.161.30:2255
|
||||
2025-12-23 23:05:07,959 - app.services - INFO - DRD stream started successfully for 63.45.161.30:2255
|
||||
2025-12-23 23:05:12,535 - app.routers - ERROR - Failed to send snapshot via WebSocket: received 1005 (no status received [internal]); then sent 1005 (no status received [internal])
|
||||
2025-12-23 23:05:12,535 - app.services - INFO - DRD stream ended for 63.45.161.30:2255
|
||||
2025-12-23 23:05:12,535 - app.routers - ERROR - Unexpected error in WebSocket stream for nl43-1: received 1005 (no status received [internal]); then sent 1005 (no status received [internal])
|
||||
2025-12-23 23:05:12,535 - app.routers - INFO - WebSocket stream closed for unit nl43-1
|
||||
2025-12-23 23:05:13,539 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Start
|
||||
2025-12-23 23:05:13,680 - app.routers - INFO - Started measurement on unit nl43-1
|
||||
2025-12-23 23:05:26,426 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Stop
|
||||
2025-12-23 23:05:26,719 - app.routers - INFO - Stopped measurement on unit nl43-1
|
||||
2025-12-23 23:05:29,480 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On
|
||||
2025-12-23 23:05:29,639 - app.services - INFO - FTP enabled on 63.45.161.30:2255
|
||||
2025-12-23 23:05:29,639 - app.routers - INFO - Enabled FTP on unit nl43-1
|
||||
2025-12-23 23:06:10,213 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 23:06:10,213 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 23:06:17,453 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 23:06:17,454 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 23:06:28,863 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 23:06:29,072 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 23:06:29,082 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 23:06:40,433 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Stop
|
||||
2025-12-23 23:06:40,599 - app.services - ERROR - Communication error with 63.45.161.30:2255: Status error - device is in wrong state for this command
|
||||
2025-12-23 23:06:40,599 - app.routers - ERROR - Unexpected error stopping measurement on nl43-1: Status error - device is in wrong state for this command
|
||||
2025-12-23 23:06:51,860 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
|
||||
2025-12-23 23:07:04,786 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On
|
||||
2025-12-23 23:07:05,040 - app.services - INFO - FTP enabled on 63.45.161.30:2255
|
||||
2025-12-23 23:07:05,040 - app.routers - INFO - Enabled FTP on unit nl43-1
|
||||
2025-12-23 23:07:11,492 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Start
|
||||
2025-12-23 23:07:12,118 - app.routers - INFO - Started measurement on unit nl43-1
|
||||
2025-12-23 23:07:15,845 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
|
||||
2025-12-23 23:07:23,413 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Stop
|
||||
2025-12-23 23:07:23,639 - app.routers - INFO - Stopped measurement on unit nl43-1
|
||||
2025-12-23 23:07:28,571 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
|
||||
2025-12-23 23:07:29,502 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP?
|
||||
2025-12-23 23:07:29,609 - app.services - INFO - FTP status on 63.45.161.30:2255: On
|
||||
2025-12-23 23:07:33,406 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,Off
|
||||
2025-12-23 23:07:33,559 - app.services - INFO - FTP disabled on 63.45.161.30:2255
|
||||
2025-12-23 23:07:33,559 - app.routers - INFO - Disabled FTP on unit nl43-1
|
||||
2025-12-23 23:07:34,407 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP?
|
||||
2025-12-23 23:07:34,565 - app.services - INFO - FTP status on 63.45.161.30:2255: Off
|
||||
2025-12-23 23:07:35,926 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On
|
||||
2025-12-23 23:07:36,129 - app.services - INFO - FTP enabled on 63.45.161.30:2255
|
||||
2025-12-23 23:07:36,129 - app.routers - INFO - Enabled FTP on unit nl43-1
|
||||
2025-12-23 23:07:37,244 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP?
|
||||
2025-12-23 23:07:37,359 - app.services - INFO - FTP status on 63.45.161.30:2255: On
|
||||
2025-12-23 23:07:45,658 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
|
||||
2025-12-23 23:07:58,073 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
|
||||
2025-12-23 23:09:02,738 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: [Errno 110] Connection timed out
|
||||
2025-12-23 23:09:02,738 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: [Errno 110] Connection timed out
|
||||
2025-12-23 23:09:25,266 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: [Errno 110] Connection timed out
|
||||
2025-12-23 23:09:25,267 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: [Errno 110] Connection timed out
|
||||
2025-12-23 23:09:39,602 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: [Errno 110] Connection timed out
|
||||
2025-12-23 23:09:39,606 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: [Errno 110] Connection timed out
|
||||
2025-12-23 23:09:55,986 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: [Errno 110] Connection timed out
|
||||
2025-12-23 23:09:55,987 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: [Errno 110] Connection timed out
|
||||
2025-12-23 23:10:08,274 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: [Errno 110] Connection timed out
|
||||
2025-12-23 23:10:08,275 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: [Errno 110] Connection timed out
|
||||
2025-12-23 23:10:09,211 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 23:10:09,211 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 23:16:04,065 - app.routers - INFO - WebSocket connection accepted for unit nl43-1
|
||||
2025-12-23 23:16:04,066 - app.routers - INFO - Starting DRD stream for unit nl43-1
|
||||
2025-12-23 23:16:04,066 - app.services - INFO - Starting DRD stream for 63.45.161.30:2255
|
||||
2025-12-23 23:16:04,239 - app.services - INFO - DRD stream started successfully for 63.45.161.30:2255
|
||||
2025-12-23 23:17:19,332 - app.routers - ERROR - Failed to send snapshot via WebSocket: received 1005 (no status received [internal]); then sent 1005 (no status received [internal])
|
||||
2025-12-23 23:17:19,333 - app.services - INFO - DRD stream ended for 63.45.161.30:2255
|
||||
2025-12-23 23:17:19,333 - app.routers - ERROR - Unexpected error in WebSocket stream for nl43-1: received 1005 (no status received [internal]); then sent 1005 (no status received [internal])
|
||||
2025-12-23 23:17:19,333 - app.routers - INFO - WebSocket stream closed for unit nl43-1
|
||||
2025-12-23 23:17:21,479 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Start
|
||||
2025-12-23 23:17:21,639 - app.routers - INFO - Started measurement on unit nl43-1
|
||||
2025-12-23 23:17:30,738 - app.services - INFO - Sending command to 63.45.161.30:2255: Manual Store
|
||||
2025-12-23 23:17:30,887 - app.services - ERROR - Communication error with 63.45.161.30:2255: Command error - device did not recognize command
|
||||
2025-12-23 23:17:30,888 - app.routers - ERROR - Unexpected error storing data on nl43-1: Command error - device did not recognize command
|
||||
2025-12-23 23:17:37,891 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Stop
|
||||
2025-12-23 23:17:38,127 - app.routers - INFO - Stopped measurement on unit nl43-1
|
||||
2025-12-23 23:17:41,793 - app.services - INFO - Sending command to 63.45.161.30:2255: Manual Store
|
||||
2025-12-23 23:17:41,967 - app.services - ERROR - Communication error with 63.45.161.30:2255: Command error - device did not recognize command
|
||||
2025-12-23 23:17:41,967 - app.routers - ERROR - Unexpected error storing data on nl43-1: Command error - device did not recognize command
|
||||
2025-12-23 23:19:35,329 - app.routers - INFO - WebSocket connection accepted for unit nl43-1
|
||||
2025-12-23 23:19:35,330 - app.routers - INFO - Starting DRD stream for unit nl43-1
|
||||
2025-12-23 23:19:35,330 - app.services - INFO - Starting DRD stream for 63.45.161.30:2255
|
||||
2025-12-23 23:19:35,479 - app.services - INFO - DRD stream started successfully for 63.45.161.30:2255
|
||||
2025-12-23 23:20:27,813 - app.routers - ERROR - Failed to send snapshot via WebSocket: no close frame received or sent
|
||||
2025-12-23 23:20:27,814 - app.services - INFO - DRD stream ended for 63.45.161.30:2255
|
||||
2025-12-23 23:20:27,814 - app.routers - ERROR - Unexpected error in WebSocket stream for nl43-1: no close frame received or sent
|
||||
2025-12-23 23:20:27,814 - app.routers - INFO - WebSocket stream closed for unit nl43-1
|
||||
2025-12-23 23:30:30,336 - app.routers - INFO - WebSocket connection accepted for unit nl43-1
|
||||
2025-12-23 23:30:30,337 - app.routers - INFO - Starting DRD stream for unit nl43-1
|
||||
2025-12-23 23:30:30,337 - app.services - INFO - Starting DRD stream for 63.45.161.30:2255
|
||||
2025-12-23 23:30:30,479 - app.services - INFO - DRD stream started successfully for 63.45.161.30:2255
|
||||
2025-12-23 23:30:32,067 - app.routers - ERROR - Failed to send snapshot via WebSocket: received 1005 (no status received [internal]); then sent 1005 (no status received [internal])
|
||||
2025-12-23 23:30:32,067 - app.services - INFO - DRD stream ended for 63.45.161.30:2255
|
||||
2025-12-23 23:30:32,067 - app.routers - ERROR - Unexpected error in WebSocket stream for nl43-1: received 1005 (no status received [internal]); then sent 1005 (no status received [internal])
|
||||
2025-12-23 23:30:32,067 - app.routers - INFO - WebSocket stream closed for unit nl43-1
|
||||
2025-12-23 23:30:33,750 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP?
|
||||
2025-12-23 23:30:33,920 - app.services - INFO - FTP status on 63.45.161.30:2255: On
|
||||
2025-12-23 23:30:36,071 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
|
||||
2025-12-23 23:30:48,848 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Start
|
||||
2025-12-23 23:30:48,999 - app.routers - INFO - Started measurement on unit nl43-1
|
||||
2025-12-23 23:30:53,748 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Stop
|
||||
2025-12-23 23:30:54,039 - app.routers - INFO - Stopped measurement on unit nl43-1
|
||||
2025-12-23 23:30:56,037 - app.services - INFO - Sending command to 63.45.161.30:2255: Manual Store
|
||||
2025-12-23 23:30:56,199 - app.services - ERROR - Communication error with 63.45.161.30:2255: Command error - device did not recognize command
|
||||
2025-12-23 23:30:56,200 - app.routers - ERROR - Unexpected error storing data on nl43-1: Command error - device did not recognize command
|
||||
2025-12-23 23:30:58,439 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Start
|
||||
2025-12-23 23:30:58,608 - app.routers - INFO - Started measurement on unit nl43-1
|
||||
2025-12-23 23:31:01,019 - app.services - INFO - Sending command to 63.45.161.30:2255: Manual Store
|
||||
2025-12-23 23:31:01,159 - app.services - ERROR - Communication error with 63.45.161.30:2255: Command error - device did not recognize command
|
||||
2025-12-23 23:31:01,160 - app.routers - ERROR - Unexpected error storing data on nl43-1: Command error - device did not recognize command
|
||||
2025-12-23 23:32:33,823 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 23:32:33,823 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 23:32:44,473 - app.services - INFO - Sending command to 63.45.161.30:2255: ManualStore
|
||||
2025-12-23 23:32:44,640 - app.services - ERROR - Communication error with 63.45.161.30:2255: Command error - device did not recognize command
|
||||
2025-12-23 23:32:44,641 - app.routers - ERROR - Unexpected error storing data on nl43-1: Command error - device did not recognize command
|
||||
2025-12-23 23:32:49,360 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Stop
|
||||
2025-12-23 23:32:49,520 - app.routers - INFO - Stopped measurement on unit nl43-1
|
||||
2025-12-23 23:32:52,035 - app.services - INFO - Sending command to 63.45.161.30:2255: ManualStore
|
||||
2025-12-23 23:32:52,359 - app.services - ERROR - Communication error with 63.45.161.30:2255: Command error - device did not recognize command
|
||||
2025-12-23 23:32:52,359 - app.routers - ERROR - Unexpected error storing data on nl43-1: Command error - device did not recognize command
|
||||
2025-12-23 23:32:55,975 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 23:32:56,217 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 23:32:56,226 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 23:33:03,172 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Stop
|
||||
2025-12-23 23:33:03,328 - app.services - ERROR - Communication error with 63.45.161.30:2255: Status error - device is in wrong state for this command
|
||||
2025-12-23 23:33:03,328 - app.routers - ERROR - Unexpected error stopping measurement on nl43-1: Status error - device is in wrong state for this command
|
||||
2025-12-23 23:36:10,824 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 23:36:10,824 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 23:36:27,467 - app.services - INFO - Sending command to 63.45.161.30:2255: Manual Store,Start
|
||||
2025-12-23 23:36:27,640 - app.services - ERROR - Communication error with 63.45.161.30:2255: Status error - device is in wrong state for this command
|
||||
2025-12-23 23:36:27,640 - app.routers - ERROR - Unexpected error storing data on nl43-1: Status error - device is in wrong state for this command
|
||||
2025-12-23 23:36:42,238 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 23:36:42,238 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 23:36:50,276 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 23:36:50,449 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 23:36:50,459 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 23:36:52,133 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Start
|
||||
2025-12-23 23:36:52,399 - app.routers - INFO - Started measurement on unit nl43-1
|
||||
2025-12-23 23:36:58,920 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Stop
|
||||
2025-12-23 23:36:59,238 - app.routers - INFO - Stopped measurement on unit nl43-1
|
||||
2025-12-23 23:37:01,738 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Start
|
||||
2025-12-23 23:37:01,880 - app.routers - INFO - Started measurement on unit nl43-1
|
||||
2025-12-23 23:37:05,371 - app.services - INFO - Sending command to 63.45.161.30:2255: Manual Store,Start
|
||||
2025-12-23 23:37:05,519 - app.services - ERROR - Communication error with 63.45.161.30:2255: Status error - device is in wrong state for this command
|
||||
2025-12-23 23:37:05,520 - app.routers - ERROR - Unexpected error storing data on nl43-1: Status error - device is in wrong state for this command
|
||||
2025-12-23 23:38:36,178 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 23:38:36,178 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 23:39:24,537 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP?
|
||||
2025-12-23 23:39:24,679 - app.services - INFO - FTP status on 63.45.161.30:2255: On
|
||||
2025-12-23 23:39:30,870 - app.services - INFO - Sending command to 63.45.161.30:2255: Manual Store,Start
|
||||
2025-12-23 23:39:30,999 - app.services - ERROR - Communication error with 63.45.161.30:2255: Status error - device is in wrong state for this command
|
||||
2025-12-23 23:39:30,999 - app.routers - ERROR - Unexpected error storing data on nl43-1: Status error - device is in wrong state for this command
|
||||
2025-12-23 23:39:33,124 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 23:39:33,289 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 23:39:33,309 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 23:41:09,171 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Stop
|
||||
2025-12-23 23:41:09,519 - app.services - ERROR - Communication error with 63.45.161.30:2255: Status error - device is in wrong state for this command
|
||||
2025-12-23 23:41:09,519 - app.routers - ERROR - Unexpected error stopping measurement on nl43-1: Status error - device is in wrong state for this command
|
||||
2025-12-23 23:41:12,189 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Start
|
||||
2025-12-23 23:41:12,359 - app.routers - INFO - Started measurement on unit nl43-1
|
||||
2025-12-23 23:41:25,761 - app.services - INFO - Sending command to 63.45.161.30:2255: Manual Store,Start
|
||||
2025-12-23 23:41:26,218 - app.services - INFO - Manual store executed on 63.45.161.30:2255
|
||||
2025-12-23 23:41:26,218 - app.routers - INFO - Manual store executed on unit nl43-1
|
||||
2025-12-23 23:41:31,981 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
|
||||
2025-12-23 23:43:41,458 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: [Errno 110] Connection timed out
|
||||
2025-12-23 23:43:41,462 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: [Errno 110] Connection timed out
|
||||
2025-12-23 23:45:35,217 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 23:45:35,217 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 23:45:36,338 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 23:45:36,339 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 23:46:03,381 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
|
||||
2025-12-24 00:18:40,457 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,Off
|
||||
2025-12-24 00:18:45,458 - app.services - ERROR - Connection timeout to 63.45.161.30:2255
|
||||
2025-12-24 00:18:45,459 - app.routers - ERROR - Failed to disable FTP on nl43-1: Failed to connect to device at 63.45.161.30:2255
|
||||
2025-12-24 00:18:48,372 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On
|
||||
2025-12-24 00:18:53,373 - app.services - ERROR - Connection timeout to 63.45.161.30:2255
|
||||
2025-12-24 00:18:53,373 - app.routers - ERROR - Failed to enable FTP on nl43-1: Failed to connect to device at 63.45.161.30:2255
|
||||
2025-12-24 00:32:34,574 - app.main - INFO - Database tables initialized
|
||||
2025-12-24 00:32:34,574 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-24 00:32:50,479 - app.routers - INFO - WebSocket connection accepted for unit nl43-1
|
||||
2025-12-24 00:32:50,480 - app.routers - INFO - Starting DRD stream for unit nl43-1
|
||||
2025-12-24 00:32:50,480 - app.services - INFO - Starting DRD stream for 63.45.161.30:2255
|
||||
2025-12-24 00:32:50,617 - app.services - INFO - DRD stream started successfully for 63.45.161.30:2255
|
||||
2025-12-24 00:33:14,274 - app.routers - ERROR - Failed to send snapshot via WebSocket: received 1005 (no status received [internal]); then sent 1005 (no status received [internal])
|
||||
2025-12-24 00:33:14,274 - app.services - INFO - DRD stream ended for 63.45.161.30:2255
|
||||
2025-12-24 00:33:14,274 - app.routers - ERROR - Unexpected error in WebSocket stream for nl43-1: received 1005 (no status received [internal]); then sent 1005 (no status received [internal])
|
||||
2025-12-24 00:33:14,274 - app.routers - INFO - WebSocket stream closed for unit nl43-1
|
||||
2025-12-24 00:33:15,714 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On
|
||||
2025-12-24 00:33:15,937 - app.services - INFO - FTP enabled on 63.45.161.30:2255
|
||||
2025-12-24 00:33:15,937 - app.routers - INFO - Enabled FTP on unit nl43-1
|
||||
2025-12-24 00:33:20,974 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Start
|
||||
2025-12-24 00:33:21,220 - app.routers - INFO - Started measurement on unit nl43-1
|
||||
2025-12-24 00:33:29,089 - app.services - INFO - Sending command to 63.45.161.30:2255: Manual Store,Start
|
||||
2025-12-24 00:33:29,218 - app.services - ERROR - Communication error with 63.45.161.30:2255: Status error - device is in wrong state for this command
|
||||
2025-12-24 00:33:29,218 - app.routers - ERROR - Unexpected error storing data on nl43-1: Status error - device is in wrong state for this command
|
||||
2025-12-24 00:33:38,733 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP?
|
||||
2025-12-24 00:33:38,897 - app.services - INFO - FTP status on 63.45.161.30:2255: On
|
||||
2025-12-24 00:33:41,454 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
|
||||
2025-12-24 00:35:50,802 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: [Errno 110] Connection timed out
|
||||
2025-12-24 00:35:50,803 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: [Errno 110] Connection timed out
|
||||
2025-12-24 01:07:41,525 - app.routers - INFO - WebSocket connection accepted for unit nl43-1
|
||||
2025-12-24 01:07:41,525 - app.routers - INFO - Starting DRD stream for unit nl43-1
|
||||
2025-12-24 01:07:41,526 - app.services - INFO - Starting DRD stream for 63.45.161.30:2255
|
||||
2025-12-24 01:07:41,696 - app.services - INFO - DRD stream started successfully for 63.45.161.30:2255
|
||||
2025-12-24 01:07:57,989 - app.routers - ERROR - Failed to send snapshot via WebSocket: received 1005 (no status received [internal]); then sent 1005 (no status received [internal])
|
||||
2025-12-24 01:07:57,989 - app.services - INFO - DRD stream ended for 63.45.161.30:2255
|
||||
2025-12-24 01:07:57,989 - app.routers - ERROR - Unexpected error in WebSocket stream for nl43-1: received 1005 (no status received [internal]); then sent 1005 (no status received [internal])
|
||||
2025-12-24 01:07:57,989 - app.routers - INFO - WebSocket stream closed for unit nl43-1
|
||||
2025-12-24 01:57:24,413 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP?
|
||||
2025-12-24 01:57:24,738 - app.services - INFO - FTP status on 63.45.161.30:2255: On
|
||||
2025-12-24 01:57:29,272 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
|
||||
2025-12-24 01:57:29,578 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: Waiting for ('230', '33x') but got 530 [' Login Fail']
|
||||
2025-12-24 01:57:29,578 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: Waiting for ('230', '33x') but got 530 [' Login Fail']
|
||||
2025-12-24 01:57:38,883 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On
|
||||
2025-12-24 01:57:39,018 - app.services - INFO - FTP enabled on 63.45.161.30:2255
|
||||
2025-12-24 01:57:39,018 - app.routers - INFO - Enabled FTP on unit nl43-1
|
||||
2025-12-24 01:57:40,971 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
|
||||
2025-12-24 01:57:41,299 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: Waiting for ('230', '33x') but got 530 [' Login Fail']
|
||||
2025-12-24 01:57:41,299 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: Waiting for ('230', '33x') but got 530 [' Login Fail']
|
||||
2025-12-24 01:59:59,951 - app.main - INFO - Database tables initialized
|
||||
2025-12-24 01:59:59,951 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-24 02:00:23,252 - app.main - INFO - Database tables initialized
|
||||
2025-12-24 02:00:23,252 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-24 02:00:38,817 - app.main - INFO - Database tables initialized
|
||||
2025-12-24 02:00:38,817 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-24 02:00:52,583 - app.main - INFO - Database tables initialized
|
||||
2025-12-24 02:00:52,583 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-24 02:01:10,756 - app.main - INFO - Database tables initialized
|
||||
2025-12-24 02:01:10,756 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-24 02:01:19,001 - app.main - INFO - Database tables initialized
|
||||
2025-12-24 02:01:19,001 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-24 02:01:25,547 - app.main - INFO - Database tables initialized
|
||||
2025-12-24 02:01:25,547 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-24 02:01:51,685 - app.main - INFO - Database tables initialized
|
||||
2025-12-24 02:01:51,685 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-24 02:02:08,074 - app.main - INFO - Database tables initialized
|
||||
2025-12-24 02:02:08,074 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-24 02:02:13,115 - app.main - INFO - Database tables initialized
|
||||
2025-12-24 02:02:13,115 - app.main - INFO - CORS allowed origins: ['*']
|
||||
|
||||
59
migrate_add_ftp_credentials.py
Executable file
59
migrate_add_ftp_credentials.py
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to add FTP username and password columns to nl43_config table.
|
||||
Run this once to update existing database schema.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DB_PATH = Path(__file__).parent / "data" / "slmm.db"
|
||||
|
||||
|
||||
def migrate():
|
||||
"""Add ftp_username and ftp_password columns to nl43_config table."""
|
||||
|
||||
if not DB_PATH.exists():
|
||||
print(f"Database not found at {DB_PATH}")
|
||||
print("No migration needed - database will be created with new schema")
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if columns already exist
|
||||
cursor.execute("PRAGMA table_info(nl43_config)")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
|
||||
if "ftp_username" in columns and "ftp_password" in columns:
|
||||
print("✓ FTP credential columns already exist, no migration needed")
|
||||
return
|
||||
|
||||
# Add ftp_username column if it doesn't exist
|
||||
if "ftp_username" not in columns:
|
||||
print("Adding ftp_username column...")
|
||||
cursor.execute("ALTER TABLE nl43_config ADD COLUMN ftp_username TEXT")
|
||||
print("✓ Added ftp_username column")
|
||||
|
||||
# Add ftp_password column if it doesn't exist
|
||||
if "ftp_password" not in columns:
|
||||
print("Adding ftp_password column...")
|
||||
cursor.execute("ALTER TABLE nl43_config ADD COLUMN ftp_password TEXT")
|
||||
print("✓ Added ftp_password column")
|
||||
|
||||
conn.commit()
|
||||
print("\n✓ Migration completed successfully!")
|
||||
print("\nYou can now set FTP credentials via the web UI or database.")
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(f"✗ Migration failed: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate()
|
||||
@@ -2,3 +2,4 @@ fastapi
|
||||
uvicorn
|
||||
sqlalchemy
|
||||
pydantic
|
||||
aioftp
|
||||
|
||||
65
set_ftp_credentials.py
Executable file
65
set_ftp_credentials.py
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Helper script to set FTP credentials for a device.
|
||||
Usage: python3 set_ftp_credentials.py <unit_id> <username> <password>
|
||||
"""
|
||||
|
||||
import sys
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
DB_PATH = Path(__file__).parent / "data" / "slmm.db"
|
||||
|
||||
|
||||
def set_credentials(unit_id: str, username: str, password: str):
|
||||
"""Set FTP credentials for a device."""
|
||||
|
||||
if not DB_PATH.exists():
|
||||
print(f"Error: Database not found at {DB_PATH}")
|
||||
sys.exit(1)
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if unit exists
|
||||
cursor.execute("SELECT unit_id FROM nl43_config WHERE unit_id = ?", (unit_id,))
|
||||
if not cursor.fetchone():
|
||||
print(f"Error: Unit '{unit_id}' not found in database")
|
||||
print("\nAvailable units:")
|
||||
cursor.execute("SELECT unit_id FROM nl43_config")
|
||||
for row in cursor.fetchall():
|
||||
print(f" - {row[0]}")
|
||||
sys.exit(1)
|
||||
|
||||
# Update credentials
|
||||
cursor.execute(
|
||||
"UPDATE nl43_config SET ftp_username = ?, ftp_password = ? WHERE unit_id = ?",
|
||||
(username, password, unit_id)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
print(f"✓ FTP credentials updated for unit '{unit_id}'")
|
||||
print(f" Username: {username}")
|
||||
print(f" Password: {'*' * len(password)}")
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 4:
|
||||
print("Usage: python3 set_ftp_credentials.py <unit_id> <username> <password>")
|
||||
print("\nExample:")
|
||||
print(" python3 set_ftp_credentials.py nl43-1 admin mypassword")
|
||||
sys.exit(1)
|
||||
|
||||
unit_id = sys.argv[1]
|
||||
username = sys.argv[2]
|
||||
password = sys.argv[3]
|
||||
|
||||
set_credentials(unit_id, username, password)
|
||||
@@ -33,9 +33,25 @@
|
||||
<legend>Controls</legend>
|
||||
<button onclick="start()">Start</button>
|
||||
<button onclick="stop()">Stop</button>
|
||||
<button onclick="store()">Store Data</button>
|
||||
<button onclick="live()">Fetch Live (DOD?)</button>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Live Stream (DRD)</legend>
|
||||
<button id="streamBtn" onclick="toggleStream()">Start Stream</button>
|
||||
<span id="streamStatus" style="margin-left: 12px; color: #888;">Not connected</span>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>FTP File Download</legend>
|
||||
<button onclick="enableFTP()">Enable FTP</button>
|
||||
<button onclick="disableFTP()">Disable FTP</button>
|
||||
<button onclick="checkFTPStatus()">Check FTP Status</button>
|
||||
<button onclick="listFiles()">List Files</button>
|
||||
<div id="fileList" style="margin-top: 12px; max-height: 200px; overflow-y: auto; background: #f6f8fa; border: 1px solid #d0d7de; padding: 8px;"></div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Status</legend>
|
||||
<pre id="status">No data yet.</pre>
|
||||
@@ -49,9 +65,15 @@
|
||||
<script>
|
||||
const logEl = document.getElementById('log');
|
||||
const statusEl = document.getElementById('status');
|
||||
const streamBtn = document.getElementById('streamBtn');
|
||||
const streamStatus = document.getElementById('streamStatus');
|
||||
|
||||
let ws = null;
|
||||
let streamUpdateCount = 0;
|
||||
|
||||
function log(msg) {
|
||||
logEl.textContent += msg + "\n";
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
@@ -93,6 +115,13 @@
|
||||
log(`Stop: ${res.status}`);
|
||||
}
|
||||
|
||||
async function store() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/store`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
log(`Store: ${res.status} - ${data.message || JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
async function live() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/live`);
|
||||
@@ -104,6 +133,185 @@
|
||||
statusEl.textContent = JSON.stringify(data.data, null, 2);
|
||||
log(`Live: ${JSON.stringify(data.data)}`);
|
||||
}
|
||||
|
||||
function toggleStream() {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
stopStream();
|
||||
} else {
|
||||
startStream();
|
||||
}
|
||||
}
|
||||
|
||||
function startStream() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${wsProtocol}//${window.location.host}/api/nl43/${unitId}/stream`;
|
||||
|
||||
log(`Connecting to WebSocket: ${wsUrl}`);
|
||||
streamUpdateCount = 0;
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
log('WebSocket connected - DRD streaming started');
|
||||
streamBtn.textContent = 'Stop Stream';
|
||||
streamStatus.textContent = 'Connected';
|
||||
streamStatus.style.color = '#0a0';
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.error) {
|
||||
log(`Stream error: ${data.error} - ${data.detail || ''}`);
|
||||
return;
|
||||
}
|
||||
|
||||
streamUpdateCount++;
|
||||
|
||||
// Update status display with live data
|
||||
const displayData = {
|
||||
unit_id: data.unit_id,
|
||||
timestamp: data.timestamp,
|
||||
lp: data.lp,
|
||||
leq: data.leq,
|
||||
lmax: data.lmax,
|
||||
lmin: data.lmin,
|
||||
lpeak: data.lpeak
|
||||
};
|
||||
statusEl.textContent = JSON.stringify(displayData, null, 2);
|
||||
|
||||
// Log every 10th update to avoid spamming
|
||||
if (streamUpdateCount % 10 === 0) {
|
||||
log(`Stream update #${streamUpdateCount}: Lp=${data.lp} Leq=${data.leq} Lpeak=${data.lpeak}`);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
log('WebSocket error occurred');
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
log(`WebSocket closed (received ${streamUpdateCount} updates)`);
|
||||
streamBtn.textContent = 'Start Stream';
|
||||
streamStatus.textContent = 'Not connected';
|
||||
streamStatus.style.color = '#888';
|
||||
ws = null;
|
||||
};
|
||||
}
|
||||
|
||||
function stopStream() {
|
||||
if (ws) {
|
||||
log('Closing WebSocket...');
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (ws) ws.close();
|
||||
});
|
||||
|
||||
// FTP Functions
|
||||
async function enableFTP() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/ftp/enable`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
log(`Enable FTP: ${res.status} - ${data.message || JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
async function disableFTP() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/ftp/disable`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
log(`Disable FTP: ${res.status} - ${data.message || JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
async function checkFTPStatus() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/ftp/status`);
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
log(`FTP Status: ${data.ftp_status} (enabled: ${data.ftp_enabled})`);
|
||||
} else {
|
||||
log(`FTP Status check failed: ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function listFiles() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/ftp/files?path=/`);
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
log(`List files failed: ${res.status} ${JSON.stringify(data)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileListEl = document.getElementById('fileList');
|
||||
fileListEl.innerHTML = '';
|
||||
|
||||
if (data.files.length === 0) {
|
||||
fileListEl.textContent = 'No files found';
|
||||
log(`No files found on device`);
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Found ${data.count} files on device`);
|
||||
|
||||
data.files.forEach(file => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.style.marginBottom = '8px';
|
||||
fileDiv.style.padding = '4px';
|
||||
fileDiv.style.borderBottom = '1px solid #ddd';
|
||||
|
||||
const icon = file.is_dir ? '📁' : '📄';
|
||||
const size = file.is_dir ? '' : ` (${(file.size / 1024).toFixed(1)} KB)`;
|
||||
|
||||
fileDiv.innerHTML = `
|
||||
${icon} <strong>${file.name}</strong>${size}
|
||||
${!file.is_dir ? `<button onclick="downloadFile('${file.path}')" style="margin-left: 8px; padding: 2px 6px; font-size: 0.9em;">Download</button>` : ''}
|
||||
<br><small style="color: #666;">${file.path}</small>
|
||||
`;
|
||||
|
||||
fileListEl.appendChild(fileDiv);
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadFile(remotePath) {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
log(`Downloading file: ${remotePath}...`);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/nl43/${unitId}/ftp/download`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ remote_path: remotePath })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
log(`Download failed: ${res.status} - ${data.detail || JSON.stringify(data)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger browser download
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = remotePath.split('/').pop();
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
log(`Downloaded: ${remotePath}`);
|
||||
} catch (error) {
|
||||
log(`Download error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user