diff --git a/gui_client.py b/gui_client.py index ef51334..fbf3ca5 100644 --- a/gui_client.py +++ b/gui_client.py @@ -23,8 +23,8 @@ from PyQt5.QtCore import QObject, Qt, pyqtSignal from PyQt5.QtGui import QColor, QFont from PyQt5.QtWidgets import ( QApplication, QComboBox, QFormLayout, QGridLayout, QGroupBox, - QHBoxLayout, QLabel, QLineEdit, QMainWindow, QPushButton, QSizePolicy, - QSpinBox, QStatusBar, QTabWidget, QTreeWidget, QTreeWidgetItem, + QHBoxLayout, QLabel, QLineEdit, QMainWindow, QPushButton, QScrollArea, + QSizePolicy, QSpinBox, QStatusBar, QTabWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, ) @@ -36,6 +36,28 @@ pg.setConfigOption("foreground", "k") MAX_PLOT_POINTS = 200 _SETTINGS_PATH = Path.home() / ".config" / "we_monitor" / "plot_settings.json" +# Maps full dotted trace key → short display label used in chips, legends and +# cursor readout. Falls back to the last path component for unknown keys. +_TRACE_LABELS: dict[str, str] = { + "charging.chargingStatus.chargePower": "charge power", + "charging.chargingStatus.remainingChargingTimeToComplete": "time to full", + "charging.batteryStatus.currentSOC": "SOC", + "charging.batteryStatus.cruisingRangeElectric": "range", + "charging.chargingSettings.targetSOC": "target SOC", + "climatisation.climatisationStatus.remainingClimatisationTime": "clim. time", + "climatisation.climatisationSettings.targetTemperature": "target temp", + "measurements.odometerStatus.odometer": "odometer", + "measurements.rangeStatus.electricRange": "elec. range", + "measurements.rangeStatus.totalRange": "total range", + "measurements.temperatureBatteryStatus.temperatureHvBatteryMax": "batt. temp max", + "measurements.temperatureBatteryStatus.temperatureHvBatteryMin": "batt. temp min", + "measurements.temperatureOutsideStatus.temperatureOutside": "outside temp", +} + + +def _trace_label(key: str) -> str: + return _TRACE_LABELS.get(key, key.split(".")[-1]) + # ── TCP reader ──────────────────────────────────────────────────────────────── @@ -271,7 +293,7 @@ class DashboardTab(QWidget): # ── Plot tab ────────────────────────────────────────────────────────────────── class PlotTab(QWidget): - NUM_PLOTS = 4 + NUM_PLOTS = 8 MAX_TRACES = 4 COLORS = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728"] @@ -281,32 +303,19 @@ class PlotTab(QWidget): main_layout.setContentsMargins(4, 4, 4, 4) main_layout.setSpacing(4) - # ── toolbar ─────────────────────────────────────────────────────────── - tb = QWidget() - tb_layout = QHBoxLayout(tb) - tb_layout.setContentsMargins(0, 0, 0, 0) - reset_btn = QPushButton("Reset zoom") - reset_btn.setFixedWidth(100) - reset_btn.clicked.connect(self._reset_zoom) - tb_layout.addWidget(reset_btn) - self._cursor_label = QLabel("") - self._cursor_label.setTextFormat(Qt.RichText) - self._cursor_label.setStyleSheet("font-size: 11px; color: #333;") - tb_layout.addWidget(self._cursor_label) - tb_layout.addStretch() - main_layout.addWidget(tb) - # ── data ────────────────────────────────────────────────────────────── self._traces: list[list[str]] = [[] for _ in range(self.NUM_PLOTS)] self._saved_traces: list[list[str]] = self._load_settings() self._series: dict[str, deque] = {} self._units: dict[str, str] = {} - # ── 2×2 plot grid (each cell: control bar + plot) ───────────────────── + # ── 4×2 scrollable plot grid (each cell: control bar + plot) ───────── plot_container = QWidget() plot_grid = QGridLayout(plot_container) plot_grid.setContentsMargins(0, 0, 0, 0) plot_grid.setSpacing(4) + for row in range(self.NUM_PLOTS // 2): + plot_grid.setRowMinimumHeight(row, 260) self._plot_widgets: list[pg.PlotWidget] = [] self._vlines: list[pg.InfiniteLine] = [] @@ -328,7 +337,8 @@ class PlotTab(QWidget): ctrl_row.setSpacing(4) add_combo = QComboBox() - add_combo.setMinimumWidth(180) + add_combo.setMinimumWidth(120) + add_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) add_combo.addItem("— add trace —") add_combo.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) self._add_combos.append(add_combo) @@ -377,14 +387,17 @@ class PlotTab(QWidget): self._plot_widgets.append(pw) plot_grid.addWidget(cell, i // 2, i % 2) - main_layout.addWidget(plot_container) + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setWidget(plot_container) + main_layout.addWidget(scroll) # ── trace management ────────────────────────────────────────────────────── def _add_trace(self, plot_idx: int): - key = self._add_combos[plot_idx].currentText() + key = self._add_combos[plot_idx].currentData() traces = self._traces[plot_idx] - if key.startswith("—") or key not in self._series: + if key is None or key not in self._series: return if key in traces or len(traces) >= self.MAX_TRACES: return @@ -394,7 +407,7 @@ class PlotTab(QWidget): self._redraw() def _add_chip(self, plot_idx: int, key: str, color_idx: int): - short = key.split(".")[-1] + short = _trace_label(key) color = self.COLORS[color_idx] btn = QPushButton(f"{short} ×") btn.setFixedHeight(22) @@ -457,13 +470,13 @@ class PlotTab(QWidget): def _refresh_combos(self): signals = sorted(self._series.keys()) for combo in self._add_combos: - current = combo.currentText() + current = combo.currentData() combo.blockSignals(True) combo.clear() combo.addItem("— add trace —") for s in signals: - combo.addItem(s) - idx = combo.findText(current) + combo.addItem(_trace_label(s), s) + idx = combo.findData(current) combo.setCurrentIndex(idx if idx >= 0 else 0) combo.blockSignals(False) @@ -515,19 +528,13 @@ class PlotTab(QWidget): if not series: continue times, values = zip(*series) - label = f"{key.split('.')[-1]} [{self._units.get(key, '')}]" + label = f"{_trace_label(key)} [{self._units.get(key, '')}]" pw.plot( list(times), list(values), pen=pg.mkPen(self.COLORS[j], width=1.5), name=label, ) - # ── zoom reset ──────────────────────────────────────────────────────────── - - def _reset_zoom(self): - for pw in self._plot_widgets: - pw.getViewBox().autoRange() - # ── cursor tracing ──────────────────────────────────────────────────────── def _on_mouse_move(self, evt, plot_idx: int): @@ -537,31 +544,12 @@ class PlotTab(QWidget): if not pw.sceneBoundingRect().contains(pos): vl.setVisible(False) - self._cursor_label.setText("") return x = pw.getPlotItem().vb.mapSceneToView(pos).x() vl.setPos(x) vl.setVisible(True) - parts = [] - for j, key in enumerate(self._traces[plot_idx]): - if key not in self._series: - continue - series = list(self._series[key]) - if not series: - continue - _, nearest_v = min(series, key=lambda p: abs(p[0] - x)) - label = key.split(".")[-1] - unit = self._units.get(key, "") - color = self.COLORS[j] - parts.append( - f'' - f' {label}: {nearest_v:.2f} {unit}' - ) - - self._cursor_label.setText("   ".join(parts)) - # ── Main window ───────────────────────────────────────────────────────────────