diff --git a/backend/routers/projects.py b/backend/routers/projects.py index 87fecfe..5d538be 100644 --- a/backend/routers/projects.py +++ b/backend/routers/projects.py @@ -45,6 +45,73 @@ router = APIRouter(prefix="/api/projects", tags=["projects"]) logger = logging.getLogger(__name__) +# ============================================================================ +# RND file normalization — maps AU2 (older Rion) column names to the NL-43 +# equivalents so report generation and the web viewer work for both formats. +# AU2 files: LAeq, LAmax, LAmin, LA01, LA10, LA50, LA90, LA95, LCpeak +# NL-43 files: Leq(Main), Lmax(Main), Lmin(Main), LN1(Main) … Lpeak(Main) +# ============================================================================ + +_AU2_TO_NL43 = { + "LAeq": "Leq(Main)", + "LAmax": "Lmax(Main)", + "LAmin": "Lmin(Main)", + "LCpeak": "Lpeak(Main)", + "LA01": "LN1(Main)", + "LA10": "LN2(Main)", + "LA50": "LN3(Main)", + "LA90": "LN4(Main)", + "LA95": "LN5(Main)", + # Time column differs too + "Time": "Start Time", +} + + +def _normalize_rnd_rows(rows: list[dict]) -> tuple[list[dict], bool]: + """ + Detect AU2-format RND rows (by presence of 'LAeq' key) and remap column + names to NL-43 equivalents. Returns (normalized_rows, was_au2_format). + If already NL-43 format the rows are returned unchanged. + """ + if not rows: + return rows, False + if "LAeq" not in rows[0]: + return rows, False # already NL-43 format + + normalized = [] + for row in rows: + new_row = {} + for k, v in row.items(): + new_row[_AU2_TO_NL43.get(k, k)] = v + normalized.append(new_row) + return normalized, True + + +def _peek_rnd_headers(file_path) -> list[dict]: + """Read just the first data row of an RND file to check column names cheaply.""" + import csv as _csv + try: + with open(file_path, 'r', encoding='utf-8', errors='replace') as f: + reader = _csv.DictReader(f) + row = next(reader, None) + return [row] if row else [] + except Exception: + return [] + + +def _is_leq_file(file_path: str, rows: list[dict]) -> bool: + """ + Return True if this RND file contains Leq (15-min averaged) data. + Accepts NL-43 Leq files (_Leq_ in path) and AU2 files (LAeq column or + Leq(Main) column after normalisation). + """ + if "_Leq_" in file_path: + return True + if rows and ("LAeq" in rows[0] or "Leq(Main)" in rows[0]): + return True + return False + + # ============================================================================ # Project List & Overview # ============================================================================ @@ -1539,6 +1606,7 @@ async def view_rnd_file( "unit": unit, "metadata": metadata, "filename": file_path.name, + "is_leq": _is_leq_file(str(file_record.file_path), _peek_rnd_headers(file_path)), }) @@ -1603,11 +1671,16 @@ async def get_rnd_data( cleaned_row[cleaned_key] = cleaned_value rows.append(cleaned_row) + # Normalise AU2-format columns to NL-43 names + rows, _was_au2 = _normalize_rnd_rows(rows) + if _was_au2: + headers = list(rows[0].keys()) if rows else headers + # Detect file type (Leq vs Lp) based on columns file_type = 'unknown' if headers: header_str = ','.join(headers).lower() - if 'leq' in header_str: + if 'leq(main)' in header_str or 'laeq' in header_str: file_type = 'leq' # Time-averaged data elif 'lp(main)' in header_str or 'lp (main)' in header_str: file_type = 'lp' # Instantaneous data @@ -1714,15 +1787,7 @@ async def generate_excel_report( if not file_path.exists(): raise HTTPException(status_code=404, detail="File not found on disk") - # Validate this is a Leq file (contains '_Leq_' in path) - # Lp files (instantaneous 100ms readings) don't have the LN percentile data needed for reports - if '_Leq_' not in file_record.file_path: - raise HTTPException( - status_code=400, - detail="Reports can only be generated from Leq files (15-minute averaged data). This appears to be an Lp (instantaneous) file." - ) - - # Read and parse the Leq RND file + # Read and parse the RND file try: with open(file_path, 'r', encoding='utf-8', errors='replace') as f: content = f.read() @@ -1748,6 +1813,18 @@ async def generate_excel_report( if not rnd_rows: raise HTTPException(status_code=400, detail="No data found in RND file") + # Normalise AU2-format columns to NL-43 names + rnd_rows, _ = _normalize_rnd_rows(rnd_rows) + + # Validate this is a Leq file — Lp files lack the LN percentile data + if not _is_leq_file(file_record.file_path, rnd_rows): + raise HTTPException( + status_code=400, + detail="Reports can only be generated from Leq files (15-minute averaged data). This appears to be an Lp (instantaneous) file." + ) + + except HTTPException: + raise except Exception as e: logger.error(f"Error reading RND file: {e}") raise HTTPException(status_code=500, detail=f"Error reading file: {str(e)}") @@ -2120,14 +2197,7 @@ async def preview_report_data( if not file_path.exists(): raise HTTPException(status_code=404, detail="File not found on disk") - # Validate this is a Leq file - if '_Leq_' not in file_record.file_path: - raise HTTPException( - status_code=400, - detail="Reports can only be generated from Leq files (15-minute averaged data)." - ) - - # Read and parse the Leq RND file + # Read and parse the RND file try: with open(file_path, 'r', encoding='utf-8', errors='replace') as f: content = f.read() @@ -2153,6 +2223,16 @@ async def preview_report_data( if not rnd_rows: raise HTTPException(status_code=400, detail="No data found in RND file") + rnd_rows, _ = _normalize_rnd_rows(rnd_rows) + + if not _is_leq_file(file_record.file_path, rnd_rows): + raise HTTPException( + status_code=400, + detail="Reports can only be generated from Leq files (15-minute averaged data)." + ) + + except HTTPException: + raise except Exception as e: logger.error(f"Error reading RND file: {e}") raise HTTPException(status_code=500, detail=f"Error reading file: {str(e)}") diff --git a/templates/rnd_viewer.html b/templates/rnd_viewer.html index 24f82bb..95ca840 100644 --- a/templates/rnd_viewer.html +++ b/templates/rnd_viewer.html @@ -60,7 +60,7 @@