diff --git a/.gitignore b/.gitignore index 9d67558..234ce26 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /manuals/ +/data/ \ No newline at end of file diff --git a/app/__pycache__/main.cpython-310.pyc b/app/__pycache__/main.cpython-310.pyc index 4a56d3c..6dabad0 100644 Binary files a/app/__pycache__/main.cpython-310.pyc and b/app/__pycache__/main.cpython-310.pyc differ diff --git a/app/__pycache__/models.cpython-310.pyc b/app/__pycache__/models.cpython-310.pyc index 4ef31d3..13b9519 100644 Binary files a/app/__pycache__/models.cpython-310.pyc and b/app/__pycache__/models.cpython-310.pyc differ diff --git a/app/__pycache__/routers.cpython-310.pyc b/app/__pycache__/routers.cpython-310.pyc index 4afed07..97e9712 100644 Binary files a/app/__pycache__/routers.cpython-310.pyc and b/app/__pycache__/routers.cpython-310.pyc differ diff --git a/app/__pycache__/services.cpython-310.pyc b/app/__pycache__/services.cpython-310.pyc index b156234..3c8764f 100644 Binary files a/app/__pycache__/services.cpython-310.pyc and b/app/__pycache__/services.cpython-310.pyc differ diff --git a/app/main.py b/app/main.py index 142b17b..dd03b3a 100644 --- a/app/main.py +++ b/app/main.py @@ -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, diff --git a/app/models.py b/app/models.py index 2c0411a..df79233 100644 --- a/app/models.py +++ b/app/models.py @@ -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) diff --git a/app/routers.py b/app/routers.py index 30bbd48..3e5dff5 100644 --- a/app/routers.py +++ b/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") diff --git a/app/services.py b/app/services.py index 10b738c..3462a2f 100644 --- a/app/services.py +++ b/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)}") diff --git a/data/slmm.db b/data/slmm.db index 461e656..24c7a61 100644 Binary files a/data/slmm.db and b/data/slmm.db differ diff --git a/data/slmm.log b/data/slmm.log index 0d5dc84..b365629 100644 --- a/data/slmm.log +++ b/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: ['*'] diff --git a/migrate_add_ftp_credentials.py b/migrate_add_ftp_credentials.py new file mode 100755 index 0000000..93653c9 --- /dev/null +++ b/migrate_add_ftp_credentials.py @@ -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() diff --git a/requirements.txt b/requirements.txt index 0a4249c..f4d8c2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ fastapi uvicorn sqlalchemy pydantic +aioftp diff --git a/set_ftp_credentials.py b/set_ftp_credentials.py new file mode 100755 index 0000000..ffcb7ca --- /dev/null +++ b/set_ftp_credentials.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +""" +Helper script to set FTP credentials for a device. +Usage: python3 set_ftp_credentials.py +""" + +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 ") + 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) diff --git a/templates/index.html b/templates/index.html index 00d66b5..3b5a088 100644 --- a/templates/index.html +++ b/templates/index.html @@ -33,9 +33,25 @@ Controls + +
+ Live Stream (DRD) + + Not connected +
+ +
+ FTP File Download + + + + +
+
+
Status
No data yet.
@@ -49,9 +65,15 @@