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/
|
/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()
|
configs = db.query(NL43Config).filter_by(tcp_enabled=True).all()
|
||||||
|
|
||||||
for cfg in configs:
|
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 = {
|
status = {
|
||||||
"unit_id": cfg.unit_id,
|
"unit_id": cfg.unit_id,
|
||||||
"host": cfg.host,
|
"host": cfg.host,
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class NL43Config(Base):
|
|||||||
tcp_port = Column(Integer, default=80) # NL43 TCP control port (via RX55)
|
tcp_port = Column(Integer, default=80) # NL43 TCP control port (via RX55)
|
||||||
tcp_enabled = Column(Boolean, default=True)
|
tcp_enabled = Column(Boolean, default=True)
|
||||||
ftp_enabled = Column(Boolean, default=False)
|
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)
|
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 sqlalchemy.orm import Session
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pydantic import BaseModel, field_validator
|
from pydantic import BaseModel, field_validator
|
||||||
import logging
|
import logging
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models import NL43Config, NL43Status
|
from app.models import NL43Config, NL43Status
|
||||||
@@ -19,6 +22,8 @@ class ConfigPayload(BaseModel):
|
|||||||
tcp_port: int | None = None
|
tcp_port: int | None = None
|
||||||
tcp_enabled: bool | None = None
|
tcp_enabled: bool | None = None
|
||||||
ftp_enabled: bool | None = None
|
ftp_enabled: bool | None = None
|
||||||
|
ftp_username: str | None = None
|
||||||
|
ftp_password: str | None = None
|
||||||
web_enabled: bool | None = None
|
web_enabled: bool | None = None
|
||||||
|
|
||||||
@field_validator("host")
|
@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
|
cfg.tcp_enabled = payload.tcp_enabled
|
||||||
if payload.ftp_enabled is not None:
|
if payload.ftp_enabled is not None:
|
||||||
cfg.ftp_enabled = payload.ftp_enabled
|
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:
|
if payload.web_enabled is not None:
|
||||||
cfg.web_enabled = payload.web_enabled
|
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:
|
if not cfg.tcp_enabled:
|
||||||
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
|
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:
|
try:
|
||||||
await client.start()
|
await client.start()
|
||||||
logger.info(f"Started measurement on unit {unit_id}")
|
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:
|
if not cfg.tcp_enabled:
|
||||||
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
|
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:
|
try:
|
||||||
await client.stop()
|
await client.stop()
|
||||||
logger.info(f"Stopped measurement on unit {unit_id}")
|
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"}
|
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")
|
@router.get("/{unit_id}/live")
|
||||||
async def live_status(unit_id: str, db: Session = Depends(get_db)):
|
async def live_status(unit_id: str, db: Session = Depends(get_db)):
|
||||||
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
|
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:
|
if not cfg.tcp_enabled:
|
||||||
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
|
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:
|
try:
|
||||||
snap = await client.request_dod()
|
snap = await client.request_dod()
|
||||||
snap.unit_id = unit_id
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Unexpected error getting live status for {unit_id}: {e}")
|
logger.error(f"Unexpected error getting live status for {unit_id}: {e}")
|
||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
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
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
import aioftp
|
||||||
|
|
||||||
from app.models import NL43Status
|
from app.models import NL43Status
|
||||||
|
|
||||||
@@ -69,10 +70,12 @@ _rate_limit_lock = asyncio.Lock()
|
|||||||
|
|
||||||
|
|
||||||
class NL43Client:
|
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.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
self.ftp_username = ftp_username or "anonymous"
|
||||||
|
self.ftp_password = ftp_password or ""
|
||||||
self.device_key = f"{host}:{port}"
|
self.device_key = f"{host}:{port}"
|
||||||
|
|
||||||
async def _enforce_rate_limit(self):
|
async def _enforce_rate_limit(self):
|
||||||
@@ -215,3 +218,217 @@ class NL43Client:
|
|||||||
According to NL43 protocol: Measure,Stop (no $ prefix, capitalized param)
|
According to NL43 protocol: Measure,Stop (no $ prefix, capitalized param)
|
||||||
"""
|
"""
|
||||||
await self._send_command("Measure,Stop\r\n")
|
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: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,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: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
|
uvicorn
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
pydantic
|
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>
|
<legend>Controls</legend>
|
||||||
<button onclick="start()">Start</button>
|
<button onclick="start()">Start</button>
|
||||||
<button onclick="stop()">Stop</button>
|
<button onclick="stop()">Stop</button>
|
||||||
|
<button onclick="store()">Store Data</button>
|
||||||
<button onclick="live()">Fetch Live (DOD?)</button>
|
<button onclick="live()">Fetch Live (DOD?)</button>
|
||||||
</fieldset>
|
</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>
|
<fieldset>
|
||||||
<legend>Status</legend>
|
<legend>Status</legend>
|
||||||
<pre id="status">No data yet.</pre>
|
<pre id="status">No data yet.</pre>
|
||||||
@@ -49,9 +65,15 @@
|
|||||||
<script>
|
<script>
|
||||||
const logEl = document.getElementById('log');
|
const logEl = document.getElementById('log');
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
|
const streamBtn = document.getElementById('streamBtn');
|
||||||
|
const streamStatus = document.getElementById('streamStatus');
|
||||||
|
|
||||||
|
let ws = null;
|
||||||
|
let streamUpdateCount = 0;
|
||||||
|
|
||||||
function log(msg) {
|
function log(msg) {
|
||||||
logEl.textContent += msg + "\n";
|
logEl.textContent += msg + "\n";
|
||||||
|
logEl.scrollTop = logEl.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
@@ -93,6 +115,13 @@
|
|||||||
log(`Stop: ${res.status}`);
|
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() {
|
async function live() {
|
||||||
const unitId = document.getElementById('unitId').value;
|
const unitId = document.getElementById('unitId').value;
|
||||||
const res = await fetch(`/api/nl43/${unitId}/live`);
|
const res = await fetch(`/api/nl43/${unitId}/live`);
|
||||||
@@ -104,6 +133,185 @@
|
|||||||
statusEl.textContent = JSON.stringify(data.data, null, 2);
|
statusEl.textContent = JSON.stringify(data.data, null, 2);
|
||||||
log(`Live: ${JSON.stringify(data.data)}`);
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user