# Copyright (C) 2026 Hector van der Aa # Copyright (C) 2026 Association Exergie # SPDX-License-Identifier: GPL-3.0-or-later from bisect import bisect_right from collections import defaultdict import csv import io from pathlib import Path from threading import Lock from uuid import UUID class ValueBuffer: def __init__(self) -> None: self._samples: list[tuple[int, UUID, float]] = [] self._lock = Lock() def append(self, timestamp: int, uuid: UUID, value: float) -> None: with self._lock: self._samples.append((timestamp, uuid, value)) def clear(self) -> None: with self._lock: self._samples.clear() def get_signal_ids(self) -> list[UUID]: with self._lock: return list({signal_id for _, signal_id, _ in self._samples}) def __len__(self) -> int: with self._lock: return len(self._samples) def normalize(self) -> None: if len(self) == 0: return with self._lock: minimum = min(sample[0] for sample in self._samples) self._samples = [ (timestamp - minimum, signal_id, value) for timestamp, signal_id, value in self._samples ] def sort(self) -> None: with self._lock: self._samples.sort(key=lambda sample: sample[0]) def import_csv(self, input: str) -> None: reader = csv.reader(io.StringIO(input, newline="")) header = next(reader, None) if header is None: raise ValueError("CSV input is empty") if not header or header[0] != "timestamp_ns": raise ValueError("The first CSV column must be 'timestamp_ns'") try: signal_ids = [UUID(column) for column in header[1:]] except ValueError as error: raise ValueError( "CSV contains an invalid signal UUID in its header" ) from error if len(signal_ids) != len(set(signal_ids)): raise ValueError("CSV contains duplicate signal UUID columns") imported_samples: list[tuple[int, UUID, float]] = [] for line_number, row in enumerate(reader, start=2): if not row or all(cell == "" for cell in row): continue if len(row) != len(header): raise ValueError( f"Line {line_number} has {len(row)} columns; expected {len(header)}" ) try: timestamp_ns = int(row[0]) except ValueError as error: raise ValueError( f"Line {line_number} has an invalid timestamp: {row[0]!r}" ) from error for signal_id, cell in zip(signal_ids, row[1:]): if cell == "": continue try: value = float(cell) except ValueError as error: raise ValueError( f"Line {line_number} has an invalid value: {cell!r}" ) from error imported_samples.append((timestamp_ns, signal_id, value)) imported_samples.sort(key=lambda sample: sample[0]) with self._lock: self._samples = imported_samples def export_csv(self) -> str: with self._lock: samples = list(self._samples) rows: dict[int, dict[UUID, float]] = defaultdict(dict) signal_ids: set[UUID] = set() for timestamp_ns, signal_id, value in samples: rows[timestamp_ns][signal_id] = value signal_ids.add(signal_id) ordered_signal_ids = sorted(signal_ids, key=str) output = io.StringIO(newline="") writer = csv.writer(output) writer.writerow( ["timestamp_ns", *(str(signal_id) for signal_id in ordered_signal_ids)] ) for timestamp_ns in sorted(rows): timestamp_values = rows[timestamp_ns] writer.writerow( [ timestamp_ns, *( timestamp_values.get(signal_id, "") for signal_id in ordered_signal_ids ), ] ) return output.getvalue() def get_samples_from(self, timestamp: int) -> list[tuple[int, UUID, float]]: with self._lock: index = bisect_right( self._samples, timestamp, key=lambda sample: sample[0], ) return self._samples[index:].copy()