feat: add support for nl32 data in webviewer and report generator.

This commit is contained in:
2026-03-05 04:19:34 +00:00
parent bd3d937a82
commit 7fde14d882
2 changed files with 99 additions and 19 deletions

View File

@@ -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)}")

View File

@@ -60,7 +60,7 @@
</div>
<div class="flex items-center gap-3">
{# Only show Report button for Leq files (15-min averaged data with LN percentiles) #}
{% if file and '_Leq_' in file.file_path %}
{% if is_leq %}
<!-- Generate Excel Report Button -->
<button onclick="openReportModal()"
class="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors flex items-center gap-2">