Implement automatic sleep mode disable for NL43/NL53 during config updates and measurements
This commit is contained in:
@@ -18,6 +18,28 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/nl43", tags=["nl43"])
|
||||
|
||||
|
||||
async def ensure_sleep_mode_disabled(client: NL43Client, unit_id: str):
|
||||
"""
|
||||
Helper function to ensure sleep mode is disabled on the device.
|
||||
Sleep/eco mode turns off TCP communications, preventing remote monitoring.
|
||||
This should be called when configuring a device or starting measurements.
|
||||
"""
|
||||
try:
|
||||
current_status = await client.get_sleep_status()
|
||||
logger.info(f"Current sleep mode status for {unit_id}: {current_status}")
|
||||
|
||||
# If sleep mode is on, disable it
|
||||
if "On" in current_status or "on" in current_status:
|
||||
logger.info(f"Sleep mode is enabled on {unit_id}, disabling it to maintain TCP connectivity")
|
||||
await client.wake()
|
||||
logger.info(f"Successfully disabled sleep mode on {unit_id}")
|
||||
else:
|
||||
logger.info(f"Sleep mode already disabled on {unit_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not verify/disable sleep mode on {unit_id}: {e}")
|
||||
# Don't raise - we want configuration to succeed even if sleep mode check fails
|
||||
|
||||
|
||||
class ConfigPayload(BaseModel):
|
||||
host: str | None = None
|
||||
tcp_port: int | None = None
|
||||
@@ -77,7 +99,7 @@ def get_config(unit_id: str, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.put("/{unit_id}/config")
|
||||
def upsert_config(unit_id: str, payload: ConfigPayload, db: Session = Depends(get_db)):
|
||||
async def upsert_config(unit_id: str, payload: ConfigPayload, db: Session = Depends(get_db)):
|
||||
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
|
||||
if not cfg:
|
||||
cfg = NL43Config(unit_id=unit_id)
|
||||
@@ -103,6 +125,14 @@ def upsert_config(unit_id: str, payload: ConfigPayload, db: Session = Depends(ge
|
||||
db.commit()
|
||||
db.refresh(cfg)
|
||||
logger.info(f"Updated config for unit {unit_id}")
|
||||
|
||||
# If TCP is enabled and we have connection details, automatically disable sleep mode
|
||||
# to ensure TCP communications remain available
|
||||
if cfg.tcp_enabled and cfg.host and cfg.tcp_port:
|
||||
logger.info(f"TCP enabled for {unit_id}, ensuring sleep mode is disabled")
|
||||
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password, ftp_port=cfg.ftp_port or 21)
|
||||
await ensure_sleep_mode_disabled(client, unit_id)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"data": {
|
||||
@@ -200,6 +230,10 @@ async def start_measurement(unit_id: str, db: Session = Depends(get_db)):
|
||||
|
||||
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password, ftp_port=cfg.ftp_port or 21)
|
||||
try:
|
||||
# Ensure sleep mode is disabled before starting measurement
|
||||
# Sleep mode would interrupt TCP communications
|
||||
await ensure_sleep_mode_disabled(client, unit_id)
|
||||
|
||||
await client.start()
|
||||
logger.info(f"Started measurement on unit {unit_id}")
|
||||
|
||||
@@ -962,6 +996,49 @@ async def download_ftp_file(unit_id: str, payload: DownloadRequest, db: Session
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.post("/{unit_id}/ftp/download-folder")
|
||||
async def download_ftp_folder(unit_id: str, payload: DownloadRequest, db: Session = Depends(get_db)):
|
||||
"""Download an entire folder from the device via FTP as a ZIP archive.
|
||||
|
||||
The folder is recursively downloaded and packaged into a ZIP file.
|
||||
Useful for downloading complete measurement sessions (e.g., Auto_0000 folders).
|
||||
"""
|
||||
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 folder name from remote path
|
||||
folder_name = os.path.basename(payload.remote_path.rstrip('/'))
|
||||
if not folder_name:
|
||||
raise HTTPException(status_code=400, detail="Invalid remote path")
|
||||
|
||||
# Generate ZIP filename
|
||||
zip_filename = f"{folder_name}.zip"
|
||||
zip_path = os.path.join(download_dir, zip_filename)
|
||||
|
||||
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password, ftp_port=cfg.ftp_port or 21)
|
||||
try:
|
||||
await client.download_ftp_folder(payload.remote_path, zip_path)
|
||||
logger.info(f"Downloaded folder {payload.remote_path} from {unit_id} to {zip_path}")
|
||||
|
||||
# Return the ZIP file
|
||||
return FileResponse(
|
||||
path=zip_path,
|
||||
filename=zip_filename,
|
||||
media_type="application/zip",
|
||||
)
|
||||
except ConnectionError as e:
|
||||
logger.error(f"Failed to download folder 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 folder from {unit_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
# Timing/Interval Configuration Endpoints
|
||||
|
||||
@router.get("/{unit_id}/measurement-time")
|
||||
|
||||
104
app/services.py
104
app/services.py
@@ -10,6 +10,8 @@ import contextlib
|
||||
import logging
|
||||
import time
|
||||
import os
|
||||
import zipfile
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional, List
|
||||
@@ -850,3 +852,105 @@ class NL43Client:
|
||||
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)}")
|
||||
|
||||
async def download_ftp_folder(self, remote_path: str, zip_path: str):
|
||||
"""Download an entire folder from the device via FTP as a ZIP archive.
|
||||
|
||||
Recursively downloads all files and subdirectories in the specified folder
|
||||
and packages them into a ZIP file. This is useful for downloading complete
|
||||
measurement sessions (e.g., Auto_0000 folders with all their contents).
|
||||
|
||||
Args:
|
||||
remote_path: Full path to folder on the device (e.g., "/NL-43/Auto_0000")
|
||||
zip_path: Local path where the ZIP file will be saved
|
||||
"""
|
||||
logger.info(f"Downloading folder {remote_path} from {self.device_key} as ZIP to {zip_path}")
|
||||
|
||||
def _download_folder_sync():
|
||||
"""Synchronous FTP folder download and ZIP creation."""
|
||||
ftp = FTP()
|
||||
ftp.set_debuglevel(0)
|
||||
|
||||
# Create a temporary directory for downloaded files
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
try:
|
||||
# Connect and login
|
||||
ftp.connect(self.host, self.ftp_port, timeout=10)
|
||||
ftp.login(self.ftp_username, self.ftp_password)
|
||||
ftp.set_pasv(False) # Force active mode
|
||||
|
||||
def download_recursive(ftp_path: str, local_path: str):
|
||||
"""Recursively download files and directories."""
|
||||
logger.info(f"Processing folder: {ftp_path}")
|
||||
|
||||
# Create local directory
|
||||
os.makedirs(local_path, exist_ok=True)
|
||||
|
||||
# List contents
|
||||
try:
|
||||
items = []
|
||||
ftp.cwd(ftp_path)
|
||||
ftp.retrlines('LIST', items.append)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list {ftp_path}: {e}")
|
||||
return
|
||||
|
||||
for item in items:
|
||||
# Parse FTP LIST output (Unix-style)
|
||||
parts = item.split(None, 8)
|
||||
if len(parts) < 9:
|
||||
continue
|
||||
|
||||
permissions = parts[0]
|
||||
name = parts[8]
|
||||
|
||||
# Skip . and .. entries
|
||||
if name in ['.', '..']:
|
||||
continue
|
||||
|
||||
is_dir = permissions.startswith('d')
|
||||
full_remote_path = f"{ftp_path}/{name}".replace('//', '/')
|
||||
full_local_path = os.path.join(local_path, name)
|
||||
|
||||
if is_dir:
|
||||
# Recursively download subdirectory
|
||||
download_recursive(full_remote_path, full_local_path)
|
||||
else:
|
||||
# Download file
|
||||
try:
|
||||
logger.info(f"Downloading file: {full_remote_path}")
|
||||
with open(full_local_path, 'wb') as f:
|
||||
ftp.retrbinary(f'RETR {full_remote_path}', f.write)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download {full_remote_path}: {e}")
|
||||
|
||||
# Download entire folder structure
|
||||
folder_name = os.path.basename(remote_path.rstrip('/'))
|
||||
local_folder = os.path.join(temp_dir, folder_name)
|
||||
download_recursive(remote_path, local_folder)
|
||||
|
||||
# Create ZIP archive
|
||||
logger.info(f"Creating ZIP archive: {zip_path}")
|
||||
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for root, dirs, files in os.walk(local_folder):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
# Calculate relative path for ZIP archive
|
||||
arcname = os.path.relpath(file_path, temp_dir)
|
||||
zipf.write(file_path, arcname)
|
||||
logger.info(f"Added to ZIP: {arcname}")
|
||||
|
||||
logger.info(f"Successfully created ZIP archive: {zip_path}")
|
||||
|
||||
finally:
|
||||
try:
|
||||
ftp.quit()
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Run synchronous FTP folder download in thread pool
|
||||
await asyncio.to_thread(_download_folder_sync)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download folder {remote_path} from {self.device_key}: {e}")
|
||||
raise ConnectionError(f"FTP folder download failed: {str(e)}")
|
||||
|
||||
Reference in New Issue
Block a user