Table of Contents
ionpy – Vollständige Implementierungs-Roadmap
Device Onboarding Wizard & offene Punkte
Erstellt: 2026-02-27
Status: In Planung
Projekt: ionpy Pro OS
Legende
- 🔴 Muss – Blocker, ohne das geht es nicht weiter
- 🟠 Soll – Wichtig, sollte in diesem Zyklus erledigt werden
- 🟡 Kann – Nice-to-have, verschiebbar
- ✅ Erledigt
- ⏳ In Arbeit
PHASE 0: Sofort-Fixes vor allem anderen
(~2-3 Tage – kleine Aufwände, große Wirkung)
Diese Tasks sind Einzeiler oder Kleinstaufwände die vor jedem weiteren Schritt erledigt werden müssen, weil sie sonst zu Crashes oder Memory-Leaks führen.
0.1 EventBus Queue maxsize
Datei: core/event_bus.py
Aufwand: 5 Minuten
Priorität: 🔴 Muss
Problem:
# AKTUELL - kein maxsize = unbegrenztes RAM-Wachstum def subscribe(self) -> asyncio.Queue: q = asyncio.Queue() # ← KEIN LIMIT! self.subscribers.append(q) return q
Fix:
def subscribe(self, maxsize: int = 500) -> asyncio.Queue: q = asyncio.Queue(maxsize=maxsize) self.subscribers.append(q) return q
Hinweis für KI: Nur diese eine Zeile ändern. maxsize=500 ist ein guter Startwert – entspricht ca. 5 Sekunden Daten bei 100 Paketen/Sekunde.
0.2 ResizeObserver Memory Leak
Datei: static/views/chart_view.js
Aufwand: 15 Minuten
Priorität: 🔴 Muss
Problem:
// onMounted - Observer wird erstellt... const resizeObserver = new ResizeObserver(() => { ... }); resizeObserver.observe(chartWrapper.value); // onUnmounted - Observer wird NIE getrennt! onUnmounted(() => { if (metronomeWorker) { ... } uplotInstances.forEach(inst => inst.destroy()); // ← resizeObserver.disconnect() FEHLT });
Fix:
// resizeObserver als Variable außerhalb von onMounted deklarieren let resizeObserver = null; onMounted(() => { // ...bestehender Code... resizeObserver = new ResizeObserver(() => { // ...bestehender Code... }); if (chartWrapper.value) resizeObserver.observe(chartWrapper.value); }); onUnmounted(() => { if (resizeObserver) resizeObserver.disconnect(); // ← NEU if (metronomeWorker) { metronomeWorker.postMessage('stop'); metronomeWorker.terminate(); } if (workerUrl) URL.revokeObjectURL(workerUrl); uplotInstances.forEach(inst => inst.destroy()); });
0.3 header_framer.py Stub erstellen
Datei: devices/framing/header_framer.py (NEU)
Aufwand: 1 Stunde
Priorität: 🔴 Muss
Problem: In TransportFactory registriert aber Datei
existiert nicht → ImportError bei erstem Aufruf.
Fix – Minimaler Stub der nicht crasht:
""" devices/framing/header_framer.py Stub-Implementierung. Noch nicht produktionsreif. Verhindert ImportError wenn header-Framing konfiguriert wird. """ import asyncio from devices.framing.base import AbstractFraming class HeaderFramer(AbstractFraming): """ Längen-Header basiertes Framing. Format: [4 Byte Länge (Big Endian)] [Payload] TODO: Vollständig implementieren """ def __init__(self, transport, header_size: int = 4, byte_order: str = "big"): super().__init__(transport) self.header_size = header_size self.byte_order = byte_order self.logger.warning( "HeaderFramer ist noch nicht vollständig implementiert!" ) async def get_next_frame(self, timeout: float = 5.0) -> tuple[bytes, float]: import time start = asyncio.get_event_loop().time() # Header lesen while len(self.buffer) < self.header_size: if asyncio.get_event_loop().time() - start > timeout: raise asyncio.TimeoutError("Timeout beim Lesen des Headers") new_data = await self.transport.read( self.header_size, timeout=0.1 ) if new_data: self.buffer.extend(new_data) # Länge aus Header lesen length = int.from_bytes( self.buffer[:self.header_size], byteorder=self.byte_order ) del self.buffer[:self.header_size] # Payload lesen while len(self.buffer) < length: if asyncio.get_event_loop().time() - start > timeout: raise asyncio.TimeoutError("Timeout beim Lesen des Payloads") new_data = await self.transport.read(length, timeout=0.1) if new_data: self.buffer.extend(new_data) else: await asyncio.sleep(0.01) frame_time = time.time() frame = bytes(self.buffer[:length]) del self.buffer[:length] return frame, frame_time async def put_frame(self, payload: bytes): length = len(payload).to_bytes( self.header_size, byteorder=self.byte_order ) await self.transport.write(length + payload)
0.4 HID Transport aus Wizard ausblenden
Datei: devices/transport_factory.py
Aufwand: 30 Minuten
Priorität: 🔴 Muss
Problem: hid_transport.py ist als UNFERTIG markiert.
Sobald der Wizard HID als Option anzeigt und der User es wählt
→ Crash.
Kurzfristiger Fix – Flag hinzufügen:
# devices/transport_factory.py TRANSPORT_REGISTRY = { "serial": "devices.transport.serial.Serial", "tcp": "devices.transport.tcp_client.TCPClient", "hid": "devices.transport.hid_transport.HIDTransport", "virtual": "devices.transport.virtual.VirtualTransport" } # NEU: Welche Transports sind wizard-fähig (fertig implementiert)? WIZARD_AVAILABLE_TRANSPORTS = { "serial": True, "tcp": True, "hid": False, # ← Noch nicht fertig "virtual": True }
Der Wizard-Endpoint filtert dann:
# web/rest.py - in get_available_drivers() oder separatem Endpoint from devices.transport_factory import TransportFactory, WIZARD_AVAILABLE_TRANSPORTS @router.get("/api/config/drivers/{name}/blueprint") def get_driver_blueprint(name: str): driver_class = _find_driver_class(name) if not driver_class: raise HTTPException(404, "Treiber nicht gefunden") blueprints = getattr(driver_class, 'STACK_BLUEPRINTS', {}) # Nur verfügbare Transports zurückgeben available = { k: v for k, v in blueprints.items() if WIZARD_AVAILABLE_TRANSPORTS.get(k, False) } return {"driver": name, "blueprints": available}
0.5 Strukturierte Fehlerklassen
Datei: core/exceptions.py (NEU)
Aufwand: 1 Tag
Priorität: 🔴 Muss (Wizard braucht strukturiertes Feedback)
""" core/exceptions.py Zentrale Fehlerklassen für ionpy. Ermöglicht strukturiertes Fehler-Feedback vom Backend ans Frontend. """ class IonpyBaseError(Exception): """Basis für alle ionpy Fehler.""" def __init__(self, message: str, hint: str = None, code: str = None): super().__init__(message) self.message = message self.hint = hint # Benutzerfreundlicher Tipp self.code = code # Maschinenlesbarer Fehlercode def to_dict(self) -> dict: return { "error": self.__class__.__name__, "code": self.code, "message": self.message, "hint": self.hint } class DeviceSpawnError(IonpyBaseError): """Fehler beim Erstellen eines Geräts.""" def __init__(self, device_id: str, stage: str, message: str, hint: str = None): super().__init__(message, hint, code=f"SPAWN_{stage.upper()}") self.device_id = device_id self.stage = stage # stage: "transport" | "framing" | "driver" | "connect" | "poll" def to_dict(self) -> dict: d = super().to_dict() d["device_id"] = self.device_id d["stage"] = self.stage return d class DeviceNotFoundError(IonpyBaseError): def __init__(self, device_id: str): super().__init__( f"Gerät '{device_id}' nicht gefunden", hint="Prüfe ob das Gerät in der config.yaml eingetragen ist", code="DEVICE_NOT_FOUND" ) self.device_id = device_id class DeviceDuplicateError(IonpyBaseError): def __init__(self, device_id: str): super().__init__( f"Geräte-ID '{device_id}' existiert bereits", hint="Wähle eine andere eindeutige Geräte-ID", code="DEVICE_DUPLICATE" ) class TransportError(IonpyBaseError): def __init__(self, transport_type: str, message: str, hint: str = None): super().__init__(message, hint, code=f"TRANSPORT_{transport_type.upper()}") self.transport_type = transport_type class ConfigValidationError(IonpyBaseError): def __init__(self, field: str, message: str): super().__init__( f"Konfigurationsfehler in '{field}': {message}", code="CONFIG_INVALID" ) self.field = field # FastAPI Exception Handler registrieren # In web/server.py einbinden: # # from core.exceptions import IonpyBaseError # from fastapi.responses import JSONResponse # # @app.exception_handler(IonpyBaseError) # async def ionpy_exception_handler(request, exc: IonpyBaseError): # return JSONResponse(status_code=400, content=exc.to_dict())
PHASE 1: Fundament – State & Config
(~7 Tage)
1.1 StateManager
Datei: core/state_manager.py (NEU)
Aufwand: 1.5 Tage
Priorität: 🔴 Muss
Zweck: Trennt persistenten Laufzeit-State (poll_interval, Setpoints, User-Einstellungen) von der statischen YAML-Config. Verhindert dass Laufzeit-Änderungen nach Neustart verloren gehen.
Wichtige Implementierungshinweise für KI:
- Atomares Schreiben: immer
tmp → os.replace()– nie direkt schreiben - Dirty-Flag Pattern: nur schreiben wenn sich etwas geändert hat
- Korrupte Dateien umbenennen statt löschen (
→ .corrupt) save_now()muss synchron aufrufbar sein (fürbeforeunload-Szenario)
# Dateistruktur die erzeugt wird: # data/ # ├── device_state.json ← StateManager verwaltet das # ├── workspaces/ ← Bereits vorhanden # └── logs/ ← Bereits vorhanden # Beispiel device_state.json: # { # "DPS5005_1": { # "u_set": 12.0, # "i_set": 2.5, # "poll_interval": 0.5, # "backlight": 3 # } # }
Schnittstelle:
class StateManager: def load(self): ... def get_device_state(self, device_id: str) -> dict: ... def set_value(self, device_id: str, entity_id: str, value: Any): ... def remove_device(self, device_id: str): ... async def start_autosave(self, interval: float = 30.0): ... async def save_now(self): ... async def _write_to_disk(self): ... # atomar
1.2 Entity: persist Flag
Datei: structures/entities/base.py
Aufwand: 0.5 Tage
Priorität: 🔴 Muss
Hinweis für KI: Nur ein neues Feld persist: bool = False
in init hinzufügen. Bestehende Entities sind automatisch
nicht-persistent (sicherer Default).
class BaseEntity: def __init__( self, name: str, mode: AccessMode = AccessMode.RO, ui: Optional[UIMeta] = None, channel: str = "Ch1", persist: bool = False, # ← NEU ): # ... bestehender Code ... self.persist = persist
Faustregeln für persist-Flags in Treibern:
- Messwerte (u_out, i_out, temp, soc) →
persist=False - Einstellungen (u_set, i_set, poll_interval) →
persist=True - Verbindungsstatus (state, diag_msg) →
persist=False - output_toggle →
persist=False(Sicherheit! Kein Auto-Einschalten)
1.3 Engine: StateManager einbinden
Datei: core/engine.py
Aufwand: 0.5 Tage
Priorität: 🔴 Muss
Änderungen:
- StateManager instanziieren
start(): State vor Config ladenrun_hardware(): autosave Task startenstop():save_now()aufrufenexecute_command(): persistente Entities im StateManager speichern
async def execute_command(self, device_id: str, key: str, value: Any): dev = self.device_manager.get_device(device_id) if dev: await dev.execute_command(key, value) # NEU: Persistente Entities speichern entity = dev.entities.get(key) if entity and getattr(entity, 'persist', False): self.state_manager.set_value(device_id, key, value) else: logger.warning(f"Kommando verworfen: '{device_id}' nicht gefunden!")
1.4 ConfigManager
Datei: core/config_manager.py (NEU)
Aufwand: 1.5 Tage
Priorität: 🔴 Muss
Zweck: Einzige Klasse die die YAML liest und schreibt. Trennt Config-Persistenz vom Rest der Engine.
Schnittstelle:
class ConfigManager: def __init__(self, config_path: str): ... def load(self) -> dict: ... def get_devices(self) -> list: ... def add_device(self, device_conf: dict): ... # mit Duplikat-Check def remove_device(self, device_id: str): ... def update_device(self, device_id: str, conf: dict): ... def set_device_enabled(self, device_id: str, enabled: bool): ... def _write_atomic(self, data: dict): ... # tmp → rename def _create_backup(self): ... # → config.yaml.bak
Wichtige Implementierungshinweise für KI:
- Vor jeder Schreiboperation ein Backup anlegen
- YAML-Kommentare gehen beim Schreiben verloren (ruamel.yaml
verwenden wenn Kommentare erhalten bleiben sollen,
sonst einfaches yaml.dump ist ok) * ID-Eindeutigkeit beim add_device prüfen * ''DeviceDuplicateError'' aus ''core/exceptions.py'' werfen
1.5 DeviceManager: State restore
Datei: core/device_manager.py
Aufwand: 0.5 Tage
Priorität: 🔴 Muss
Hinweis für KI: In _spawn_device() nach der Instanziierung
des Devices _restore_device_state() aufrufen. Diese Methode
setzt nur RAM-Werte – sendet keine Hardware-Befehle.
def _restore_device_state(self, device, saved_state: dict): for entity_id, value in saved_state.items(): entity = device.entities.get(entity_id) if not entity: continue if not getattr(entity, 'persist', False): continue entity.value = value # Nur RAM, kein Hardware-Befehl!
1.6 Config-Schema Validierung
Datei: core/config_validator.py (NEU)
Aufwand: 1 Tag
Priorität: 🟠 Soll
Zweck: Tippfehler in config.yaml führen aktuell zu stillen Fehlern. Pydantic gibt klare Fehlermeldungen.
from pydantic import BaseModel, validator from typing import Optional, Literal class TransportConfig(BaseModel): type: Literal["serial", "tcp", "hid", "virtual", "visa"] params: dict = {} class FramingConfig(BaseModel): type: Literal["delimiter", "header", "modbus_rtu", "start_stop", "none"] params: dict = {} class DeviceConfig(BaseModel): id: str driver: str enabled: bool = True auto_start: bool = True alias: Optional[str] = None icon: Optional[str] = "developer-board" description: Optional[str] = "" location: Optional[str] = "" transport: TransportConfig framing: FramingConfig = FramingConfig(type="none") @validator('id') def id_must_be_valid(cls, v): if not v.replace('_', '').replace('-', '').isalnum(): raise ValueError('ID darf nur Buchstaben, Zahlen, _ und - enthalten') return v class RootConfig(BaseModel): devices: list[DeviceConfig] = [] logging: dict = {}
PHASE 2: Transport Discovery APIs
(~10 Tage, kann parallel zu Phase 1 entwickelt werden)
Neue Dateistruktur:
web/ ├── inventory/ │ ├── __init__.py ← Router zusammenbauen │ ├── serial.py ← Serial Discovery │ ├── usb.py ← USB Discovery │ ├── hid.py ← HID Discovery │ ├── network.py ← TCP Probe + Scan │ └── visa.py ← VISA/SCPI Discovery
Neue requirements:
hidapi # HID Devices pyvisa # VISA Instrumente pyvisa-py # Pure Python VISA Backend (kein NI nötig) bleak # Bluetooth BLE (optional)
2.1 Serial Discovery
Datei: web/inventory/serial.py (NEU)
Aufwand: 1 Tag
Priorität: 🔴 Muss
Endpoints:
GET /api/inventory/serial– Alle Ports mit VID/PID HintsPOST /api/inventory/serial/test– Port öffnen und testen
Wichtige Implementierungshinweise für KI:
in_useFlag: prüfen welche Ports bereits von Engine-Geräten
belegt sind (aus engine.device_manager.devices auslesen)
- VID/PID Lookup-Tabelle für bekannte Geräte einbauen
- Plattform-Exceptions sauber abfangen (Windows: Access Denied,
Linux: Permission Denied → mit Hilfetext zurückgeben)
Bekannte VID/PID Mapping (Grundstock):
KNOWN_SERIAL_DEVICES = { (0x1A86, 0x7523): {"name": "CH340 USB-Serial", "hint": "DPS5005, günstige Arduinos"}, (0x0403, 0xFA00): {"name": "FTDI FT232", "hint": "Riden RD-Serie"}, (0x10C4, 0xEA60): {"name": "CP2102 USB-Serial", "hint": "Diverse Netzteile"}, (0x067B, 0x2303): {"name": "Prolific PL2303", "hint": "Ältere Geräte"}, (0x0483, 0x5740): {"name": "STM32 Virtual COM", "hint": "STM32-basierte Geräte"}, (0x2341, 0x0043): {"name": "Arduino Uno", "hint": "Arduino / Custom Firmware"}, }
2.2 USB Device Discovery
Datei: web/inventory/usb.py (NEU)
Aufwand: 1 Tag
Priorität: 🟠 Soll
Endpoint: GET /api/inventory/usb
Hinweis für KI:
- System-Geräte und Hubs filtern (VID 0x1D6B = Linux Foundation)
- Produktname-Auslesen kann fehlschlagen ohne Admin-Rechte
→ Exception abfangen, nicht crashen
- Device-Klasse in lesbaren Text übersetzen
(0x03 = HID, 0x09 = Hub, etc.)
2.3 USB HID Discovery
Datei: web/inventory/hid.py (NEU)
Aufwand: 1 Tag
Priorität: 🟠 Soll
Endpoint: GET /api/inventory/hid
Plattform-Hinweise die der Endpoint zurückgeben soll:
PLATFORM_HINTS = { "linux": "sudo usermod -aG plugdev $USER && udev-Rule anlegen", "windows": "Treiber korrekt installiert?", "darwin": "System Preferences → Security → Input Monitoring" }
2.4 TCP Probe & Netzwerk-Scan
Datei: web/inventory/network.py (NEU)
Aufwand: 2 Tage
Priorität: 🔴 Muss (TCP) / 🟠 Soll (Scan)
Endpoints:
GET /api/inventory/tcp/probe?host=x&port=y– Einzelner TestPOST /api/inventory/tcp/scan– Scan starten (gibt scan_id zurück)GET /api/inventory/tcp/scan/{scan_id}– Ergebnis pollen
Wichtige Implementierungshinweise für KI:
- Scan läuft als
BackgroundTask– nicht blockierend! - Ergebnisse in
scan_results_cache(dict) halten - Semaphore(50) um nicht zu aggressiv zu scannen
- Subnetz auto-detektieren via
socket.getsockname() - Bekannte Instrument-Ports vorbelegen:
KNOWN_INSTRUMENT_PORTS = { 5025: "SCPI (Riden, Rigol, Keysight Standard)", 502: "Modbus TCP", 1000: "Custom TCP (CH9120 Ethernet-Module)", 23: "Telnet SCPI", 3000: "Custom TCP", }
- Scan-Cache nach X Minuten aufräumen (memory leak vermeiden)
GET /api/inventory/tcp/scanohne ID → Liste laufender Scans
2.5 VISA Discovery
Datei: web/inventory/visa.py (NEU)
Aufwand: 1.5 Tage
Priorität: 🟡 Kann
Endpoint: GET /api/inventory/visa
Hinweis für KI:
pyvisa.ResourceManager('@py')– '@py' erzwingt pure Python
Backend, kein NI-VISA nötig
- Timeout auf 2000ms setzen pro Instrument
- IDN-String parsen: “Rigol,DS1054Z,DS1ZA123,v4.04”
→ manufacturer, model, serial, firmware
2.6 Inventory Router
Datei: web/inventory/init.py (NEU)
Aufwand: 0.5 Tage
Priorität: 🔴 Muss
# web/inventory/__init__.py from fastapi import APIRouter from web.inventory import serial, usb, hid, network, visa router = APIRouter(prefix="/api/inventory", tags=["Inventory"]) router.include_router(serial.router) router.include_router(usb.router) router.include_router(hid.router) router.include_router(network.router) router.include_router(visa.router) # In web/server.py einbinden: # from web.inventory import router as inventory_router # app.include_router(inventory_router)
PHASE 3: Device Lifecycle (Hot-Reload)
(~8.5 Tage)
3.1 SystemEvent Sample Typ
Datei: structures/samples/system.py (NEU)
Aufwand: 0.5 Tage
Priorität: 🔴 Muss
from dataclasses import dataclass, field from structures.samples.base import BaseSample @dataclass(kw_only=True) class SystemEvent(BaseSample): """ System-Ereignisse die über den WebSocket ans Frontend gesendet werden. Ermöglicht Live-Updates ohne Page-Reload. """ type: str = "system_event" event: str # Siehe EVENT_TYPES unten payload: dict = field(default_factory=dict) # Definierte Event-Typen: EVENT_DEVICE_ADDED = "device_added" EVENT_DEVICE_REMOVED = "device_removed" EVENT_DEVICE_UPDATED = "device_updated" EVENT_DEVICE_ERROR = "device_error" EVENT_SERVER_RESTART = "server_restart" # Payload-Struktur pro Event: # device_added: # { "meta": {...}, "state": [...] } ← komplettes Device-Objekt # device_removed: # { "device_id": "DPS5005_1" } # device_updated: # { "device_id": "DPS5005_1", "meta": {...} } # device_error: # { "device_id": "...", "message": "...", "stage": "..." }
3.2 EventBus: SystemEvents publishen
Datei: core/event_bus.py
Aufwand: 0.5 Tage
Priorität: 🔴 Muss
# In EventBus Klasse hinzufügen: async def publish_system_event(self, event: str, payload: dict): from structures.samples.system import SystemEvent sample = SystemEvent( device_id="SYSTEM", entity_id="system", event=event, payload=payload ) await self.publish(sample)
3.3 store.js: SystemEvents verarbeiten
Datei: static/js/store.js
Aufwand: 0.5 Tage
Priorität: 🔴 Muss
// In connectWebSocket() → ws.onmessage erweitern: ws.onmessage = (e) => { window.dispatchEvent(new CustomEvent('ws-message', { detail: e.data })); const sample = JSON.parse(e.data); // NEU: System-Events abfangen if (sample.type === 'system_event') { handleSystemEvent(sample); return; // ← Nicht als normales Sample verarbeiten } // ... bestehende Messwert-Logik ... }; function handleSystemEvent(sample) { const { event, payload } = sample; if (event === 'device_added') { globalStore.devices[payload.meta.device_id] = { meta: payload.meta, state: payload.state }; console.log(`Gerät hinzugefügt: ${payload.meta.device_id}`); } else if (event === 'device_removed') { delete globalStore.devices[payload.device_id]; console.log(`Gerät entfernt: ${payload.device_id}`); } else if (event === 'device_updated') { if (globalStore.devices[payload.device_id]) { globalStore.devices[payload.device_id].meta = payload.meta; } } }
3.4 DeviceManager: add_device()
Datei: core/device_manager.py
Aufwand: 1.5 Tage
Priorität: 🔴 Muss
async def add_device(self, config: dict) -> dict: """ Fügt ein Gerät live zur Laufzeit hinzu. Speichert es in YAML und startet es sofort. Gibt das komplette Device-Manifest zurück. Wirft DeviceDuplicateError wenn ID bereits existiert. Wirft DeviceSpawnError bei Verbindungsproblemen. """ dev_id = config.get('id') # Duplikat-Check if dev_id in self.devices: from core.exceptions import DeviceDuplicateError raise DeviceDuplicateError(dev_id) # Gerät spawnen (bestehende Methode) self._spawn_device(config) device = self.devices.get(dev_id) if not device: from core.exceptions import DeviceSpawnError raise DeviceSpawnError(dev_id, "driver", "Gerät konnte nicht instanziiert werden") # Verbinden await self._connect_safe(device) # In YAML persistieren if self.config_manager: self.config_manager.add_device(config) # Frontend benachrichtigen manifest = device.get_manifest() state = [{"sensor": e.id, "value": e.value, "timestamp": e.timestamp, "alarm_state": "NORMAL"} for e in device.entities.values()] await self.bus.publish_system_event( 'device_added', {"meta": asdict(manifest), "state": state} ) return asdict(manifest)
3.5 DeviceManager: remove_device()
Datei: core/device_manager.py
Aufwand: 1.5 Tage
Priorität: 🔴 Muss
Wichtige Implementierungshinweise für KI:
- Guard-Flag setzen um Race Conditions zu vermeiden
- Tasks in dieser Reihenfolge stoppen:
1. _measure_task cancellen und awaiten
2. ''transport.disconnect()'' aufrufen
3. Aus ''devices{}'' entfernen
4. StateManager aufräumen
5. ConfigManager aufräumen
6. SystemEvent publishen
async def remove_device(self, device_id: str): device = self.devices.get(device_id) if not device: from core.exceptions import DeviceNotFoundError raise DeviceNotFoundError(device_id) # Guard gegen doppelten Aufruf if getattr(device, '_removing', False): return device._removing = True try: # 1. Loop stoppen if device._measure_task and not device._measure_task.done(): device._measure_task.cancel() try: await asyncio.wait_for( asyncio.shield(device._measure_task), timeout=3.0 ) except (asyncio.CancelledError, asyncio.TimeoutError): pass device._measure_task = None # 2. Hardware trennen try: await asyncio.wait_for( device.transport.disconnect(), timeout=3.0 ) except asyncio.TimeoutError: self.logger.warning(f"[{device_id}] Disconnect Timeout") # 3. Aus Registry entfernen del self.devices[device_id] # 4. State aufräumen if self.state_manager: self.state_manager.remove_device(device_id) # 5. Config aufräumen if self.config_manager: self.config_manager.remove_device(device_id) # 6. Frontend informieren await self.bus.publish_system_event( 'device_removed', {"device_id": device_id} ) self.logger.info(f"Gerät '{device_id}' erfolgreich entfernt") except Exception as e: self.logger.error(f"Fehler beim Entfernen von '{device_id}': {e}") raise
3.6 Probe Endpoint
Datei: web/rest.py
Aufwand: 2 Tage
Priorität: 🔴 Muss
Endpoint: POST /api/devices/probe
Konzept: Baut einen temporären Stack, instanziiert den Treiber, führt einen einzelnen Poll-Zyklus aus, räumt auf und gibt ein Schritt-für-Schritt Protokoll zurück.
Request Body:
class ProbeRequest(BaseModel): driver: str # "DPS5005" transport_type: str # "serial" transport_params: dict # {"port": "COM3", "baudrate": 9600} framing_params: dict = {} class ProbeStep(BaseModel): message: str ok: bool detail: str = None class ProbeResult(BaseModel): success: bool steps: list[ProbeStep] first_values: dict # {entity_id: {value, unit}} error: str = None duration_ms: float
Wichtige Implementierungshinweise für KI:
- Temporärer
ProbeBusder Werte in ein dict sammelt
statt sie zu publishen
- Immer disconnect im finally-Block aufrufen
- Gesamt-Timeout: 10 Sekunden maximum
_find_driver_class()als Hilfsfunktion auslagern
(wird auch von anderen Endpoints gebraucht)
- Framing aus
STACK_BLUEPRINTSdes Treibers holen
wenn nicht explizit angegeben
3.7 REST Endpoints Device Lifecycle
Datei: web/rest.py
Aufwand: 0.5 Tage
Priorität: 🔴 Muss
POST /api/devices → add_device()
DELETE /api/devices/{id} → remove_device()
PUT /api/devices/{id} → update_device() (remove + add)
PATCH /api/devices/{id}/enabled → set_device_enabled()
POST /api/devices/probe → probe_device()
GET /api/config/drivers → get_available_drivers() (existiert)
GET /api/config/drivers/{name}/blueprint → get_driver_blueprint() (NEU)
Fehlerhandling-Muster für alle Lifecycle-Endpoints:
@router.delete("/devices/{device_id}") async def delete_device(device_id: str, request: Request): engine = request.app.state.engine try: await engine.device_manager.remove_device(device_id) return {"status": "ok", "device_id": device_id} except DeviceNotFoundError as e: raise HTTPException(status_code=404, detail=e.to_dict()) except Exception as e: logger.error(f"Fehler beim Löschen von {device_id}: {e}") raise HTTPException(status_code=500, detail={"message": str(e)})
PHASE 4: Frontend Infrastruktur
(~3 Tage)
4.1 API Client Modul
Datei: static/js/api.js (NEU)
Aufwand: 1 Tag
Priorität: 🟠 Soll (erspart viel Duplikation)
Zweck: Zentraler Wrapper für alle fetch()-Calls. Fehlerhandling an einer Stelle. Alle Endpoints dokumentiert.
// static/js/api.js const BASE = ''; // Gleicher Origin async function apiFetch(path, options = {}) { const res = await fetch(BASE + path, { headers: { 'Content-Type': 'application/json', ...options.headers }, ...options }); if (!res.ok) { const err = await res.json().catch(() => ({ message: res.statusText })); throw Object.assign(new Error(err.message || 'API Fehler'), { status: res.status, code: err.code, hint: err.hint, detail: err }); } return res.json(); } export const api = { // Inventory inventory: { serial: () => apiFetch('/api/inventory/serial'), serialTest: (port, baud) => apiFetch('/api/inventory/serial/test', { method: 'POST', body: JSON.stringify({port, baudrate: baud}) }), usb: () => apiFetch('/api/inventory/usb'), hid: () => apiFetch('/api/inventory/hid'), tcpProbe: (host, port) => apiFetch( `/api/inventory/tcp/probe?host=${host}&port=${port}`), tcpScanStart: (subnet, ports) => apiFetch('/api/inventory/tcp/scan', { method: 'POST', body: JSON.stringify({subnet, ports}) }), tcpScanPoll: (scanId) => apiFetch( `/api/inventory/tcp/scan/${scanId}`), visa: () => apiFetch('/api/inventory/visa'), }, // Devices devices: { list: () => apiFetch('/api/devices'), add: (conf) => apiFetch('/api/devices', { method: 'POST', body: JSON.stringify(conf) }), remove: (id) => apiFetch(`/api/devices/${id}`, { method: 'DELETE' }), update: (id, conf) => apiFetch(`/api/devices/${id}`, { method: 'PUT', body: JSON.stringify(conf) }), enable: (id, enabled) => apiFetch( `/api/devices/${id}/enabled`, { method: 'PATCH', body: JSON.stringify({enabled}) }), probe: (conf) => apiFetch('/api/devices/probe', { method: 'POST', body: JSON.stringify(conf) }), }, // Config config: { drivers: () => apiFetch('/api/config/drivers'), blueprint: (name) => apiFetch( `/api/config/drivers/${name}/blueprint`), }, // Control (bereits vorhanden, hier zentralisiert) control: (device_id, key, value) => apiFetch('/api/control', { method: 'POST', body: JSON.stringify({device_id, key, value}) }), };
4.2 index.html: Wizard-Einstiegspunkt
Datei: static/index.html
Aufwand: 0.5 Tage
Priorität: 🔴 Muss
Änderungen:
// Import hinzufügen import DeviceWizard from '/static/components/device_wizard.js'; // In setup(): const deviceWizardOpen = ref(false); const deviceWizardEditId = ref(null); // null = Add-Modus const openDeviceWizard = (deviceId = null) => { deviceWizardEditId.value = deviceId; deviceWizardOpen.value = true; };
<!-- Im Geräte-Dropdown den toten Handler ersetzen: --> <div class="dropdown-item" @click="openDeviceWizard()"> <span><i class="mdi mdi-plus-box-outline"></i> Neues Gerät hinzufügen...</span> </div> <!-- Pro Gerät Edit-Option hinzufügen: --> <div class="dropdown-item" @click.stop="openDeviceWizard(dev.id)"> <span><i class="mdi mdi-pencil"></i> Bearbeiten...</span> </div> <!-- Wizard Modal einbinden: --> <device-wizard v-if="deviceWizardOpen" :edit-device-id="deviceWizardEditId" @close="deviceWizardOpen = false" @saved="deviceWizardOpen = false"> </device-wizard>
PHASE 5: Device Wizard UI
(~8.5 Tage – das sichtbare Ergebnis)
Neue Dateistruktur:
static/
└── components/
├── device_wizard.js ← Wizard-Container (NEU)
└── wizard/
├── step_driver.js ← Schritt 1: Treiber wählen
├── step_identity.js ← Schritt 2: Basis-Infos
├── step_transport.js ← Schritt 3: Verbindung
├── discovery_panel.js ← Scan-Helper für Schritt 3
├── step_probe.js ← Schritt 4: Verbindungstest
└── step_confirm.js ← Schritt 5: Speichern
5.1 device_wizard.js – Rahmen & Navigation
Aufwand: 1 Tag
Priorität: 🔴 Muss
State der Wizard-Komponente:
// Hält die gesamte Konfiguration die durch alle Steps aufgebaut wird const wizardState = reactive({ // Step 1 selectedDriver: null, // { name, class, description, blueprints } // Step 2 deviceId: '', alias: '', icon: 'developer-board', description: '', location: '', autoStart: true, // Step 3 transportType: 'serial', // aus availableTransports transportParams: {}, // port, baudrate ODER host, port framingParams: {}, // aus Blueprint übernommen // Step 4 probeResult: null, // Ergebnis des Verbindungstests // Meta isEditMode: false, originalDeviceId: null }); const currentStep = ref(1); const totalSteps = 5; // Schritt-Validierung: Darf der User weiter? const canGoNext = computed(() => { if (currentStep.value === 1) return !!wizardState.selectedDriver; if (currentStep.value === 2) return !!wizardState.deviceId; if (currentStep.value === 3) return isTransportConfigValid(); if (currentStep.value === 4) return wizardState.probeResult?.success; return true; });
Wichtige Implementierungshinweise für KI:
- Edit-Modus: Wenn
editDeviceIdprop gesetzt ist, bestehende
Config laden und Steps vorausfüllen
- Step 4 (Probe) automatisch starten wenn Step 3 verlassen wird
- “Überspringen” Option für den Probe-Step (falls Gerät offline
konfiguriert werden soll)
- Gleicher visueller Stil wie WidgetWizard (modal-overlay, etc.)
5.2 step_driver.js – Treiber wählen
Aufwand: 1 Tag
Priorität: 🔴 Muss
Verhalten:
- Lädt
GET /api/config/driversbeim Mount - Zeigt Treiber als Kacheln mit Icon, Name, Beschreibung
- Filter-Tabs nach Kategorie (PSU, Last, BMS, Virtuell, …)
- Suchfeld (filtert Name + Beschreibung)
- Ausgewählter Treiber wird blau hervorgehoben (wie WidgetWizard)
Hinweis für KI: Der Treiber-Katalog muss category und
icon Felder zurückgeben. Diese kommen aus den Klassen-Attributen
(AbstractDevice braucht optionale Klassenattribute dafür):
# devices/base.py - Klassenattribute ergänzen class AbstractDevice: DRIVER_CATEGORY = "generic" # "psu", "load", "bms", "virtual" DRIVER_ICON = "developer-board" # ... # In konkreten Treibern überschreiben: class DPS5005(AbstractDevice): DRIVER_CATEGORY = "psu" DRIVER_ICON = "flash"
5.3 step_identity.js – Basis-Infos
Aufwand: 0.5 Tage
Priorität: 🔴 Muss
Felder:
- Geräte-ID (auto-generiert aus Treiber-Name + Zähler,
z.B. “DPS5005_1”, editierbar)
- ID-Eindeutigkeit live prüfen (gegen
globalStore.devices) - Alias (Anzeigename)
- Icon (einfaches Textfeld mit MDI-Vorschau, z.B. “flash”)
- Beschreibung (optional)
- Standort (optional)
- Auto-Start Toggle
ID Auto-Generator:
const generateDeviceId = (driverName) => { const base = driverName.toUpperCase().replace(/[^A-Z0-9]/g, '_'); let counter = 1; while (globalStore.devices[`${base}_${counter}`]) { counter++; } return `${base}_${counter}`; };
5.4 step_transport.js – Verbindung konfigurieren
Aufwand: 2 Tage
Priorität: 🔴 Muss
Struktur:
- Tab-Leiste mit verfügbaren Transports (aus Blueprint)
- Pro Transport-Typ ein Sub-Formular
Serial Sub-Formular:
// Felder: // - Port (Dropdown, befüllt via api.inventory.serial()) // - Refresh-Button für Port-Liste // - Baudrate (Dropdown mit häufigen Werten + Custom) // - Bekanntes Gerät als Hint anzeigen wenn VID/PID matcht // - "Port testen" Button (öffnet nur, sendet nichts) // Port-Eintrag Darstellung: // COM3 - USB Serial (CH340) [DPS5005 vermutlich] ← grün // COM7 - Prolific USB-Serial [Belegt] ← grau
TCP Sub-Formular:
// Felder: // - Host (IP-Eingabe mit Validierung) // - Port (Zahlenfeld, Dropdown mit bekannten Ports) // - "Netzwerk scannen" Button → öffnet discovery_panel // - Latenz-Test Button (ping)
Erweitert-Sektion (ausgeklappt):
// Zeigt framing-Parameter aus Blueprint // Modbus: Slave ID // Delimiter: Trennzeichen // Timeouts // Diese Werte aus STACK_BLUEPRINTS vorausfüllen!
5.5 discovery_panel.js – Scan Helper
Aufwand: 1.5 Tage
Priorität: 🟠 Soll
Serial Discovery Panel:
// - Port-Liste mit VID/PID Hints // - Spalten: Port | Beschreibung | Hersteller | Bekanntes Gerät | Status // - Klick auf Zeile → übernimmt Port in step_transport // - Refresh Button // - Plattform-spezifische Permissions-Warnungen anzeigen
TCP Scan Panel:
// - Subnetz-Eingabe (auto-vorbelegt) // - Port-Auswahl (Checkboxen für bekannte Ports) // - "Scan starten" → POST /api/inventory/tcp/scan // - Fortschrittsbalken (pollt GET /api/inventory/tcp/scan/{id}) // - Ergebnis-Liste: Host | Port | Service // - Klick → übernimmt Host+Port in step_transport // - Polling-Interval: 1 Sekunde
5.6 step_probe.js – Verbindungstest
Aufwand: 1.5 Tage
Priorität: 🔴 Muss (das Herzstück des Wizards!)
Das ist der wertvollste Schritt – User sieht sofort ob es klappt.
Verhalten:
- Startet automatisch wenn der Step angezeigt wird
- Ruft
POST /api/devices/probeauf - Zeigt Schritte animiert ein (mit 300ms Verzögerung pro Schritt)
- ✅ grün / ❌ rot pro Schritt
Darstellung:
┌─────────────────────────────────────────┐ │ Verbindungstest ⏱ 2.3s │ ├─────────────────────────────────────────┤ │ ✅ Treiber 'DPS5005' geladen │ │ ✅ Stack erstellt: SERIAL via MODBUS │ │ ✅ Port COM3 gefunden │ │ ✅ Verbindung aufgebaut (9600 Baud) │ │ ✅ Modbus Request gesendet │ │ ✅ Antwort empfangen (31 Bytes) │ │ ✅ CRC korrekt │ │ ✅ 13 Messwerte empfangen │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ Erste Messwerte: │ │ │ │ u_out: 12.03 V │ │ │ │ i_out: 2.51 A │ │ │ │ p_out: 30.19 W │ │ │ └─────────────────────────────────────┘ │ │ │ │ [ ↺ Erneut versuchen ] [ Weiter → ] │ └─────────────────────────────────────────┘
Fehler-Darstellung:
│ ✅ Treiber geladen │ │ ✅ Stack erstellt │ │ ❌ Port COM3 nicht gefunden │ │ → Prüfe ob das USB-Kabel │ │ angeschlossen ist. │ │ Verfügbare Ports: COM1, COM7 │
Hinweis für KI: Fehlermeldungen vom Backend (hint-Feld aus
DeviceSpawnError) direkt anzeigen. Der User soll nicht raten
müssen was falsch ist.
5.7 step_confirm.js – Speichern
Aufwand: 1 Tag
Priorität: 🔴 Muss
Verhalten:
- Zeigt zusammenfassend alle Einstellungen
- “Gerät hinzufügen” →
POST /api/devices - Zeigt Ladeindikator während Request läuft
- Bei Erfolg: kurze Erfolgsanimation, dann Wizard schließen
- Bei Fehler: Fehlermeldung + “Zurück” Option
- Checkbox: “Weiteres Gerät hinzufügen” (schließt Wizard nicht)
Config-Objekt das gebaut wird:
const buildConfig = () => ({ id: wizardState.deviceId, driver: wizardState.selectedDriver.name, enabled: true, auto_start: wizardState.autoStart, alias: wizardState.alias || wizardState.deviceId, icon: wizardState.icon, description: wizardState.description, location: wizardState.location, transport: { type: wizardState.transportType, params: wizardState.transportParams }, framing: { type: wizardState.framingType, params: wizardState.framingParams } });
PHASE 6: Device Management UI
(~2.5 Tage – Nice-to-have nach dem Wizard)
6.1 Geräte-Verwaltungs-Panel
Datei: static/views/device_manager_view.js (NEU)
Aufwand: 1.5 Tage
Priorität: 🟠 Soll
- Als Widget spawnbar (neuer Typ im Widget-Wizard)
- Liste aller Geräte mit Status-Indikator
- Edit-Button → Wizard im Edit-Modus
- Enable/Disable Toggle (kein Löschen!)
- Delete-Button mit Bestätigungs-Dialog
- Zeigt ob Gerät gerade verbindet/fehler hat
6.2 Bestätigungs-Dialog Komponente
Datei: static/components/confirm_dialog.js (NEU)
Aufwand: 0.5 Tage
Priorität: 🟠 Soll
Generischer Dialog für Delete, Notaus, etc. Wird an mehreren Stellen gebraucht – lohnt sich als Komponente.
// Verwendung: // <confirm-dialog // v-if="showConfirm" // title="Gerät löschen?" // message="DPS5005_1 wird dauerhaft entfernt." // confirm-label="Löschen" // confirm-color="#ef4444" // @confirm="doDelete" // @cancel="showConfirm = false"> // </confirm-dialog>
6.3 Widget-Wizard: Gerät Manager Eintrag
Datei: static/components/widget_wizard.js
Aufwand: 0.5 Tage
Priorität: 🟡 Kann
// In widgetCatalogue Array hinzufügen: { id: 'device_manager', type: 'device_manager', title: 'Geräte-Verwaltung', icon: 'devices', desc: 'Geräte hinzufügen, bearbeiten und entfernen.', needs: 'none' }
Gesamtübersicht Zeitplan
| Phase | Inhalt | Tage | Priorität |
|---|---|---|---|
| 0 | Sofort-Fixes | 2-3 | 🔴 Muss zuerst |
| 1 | Fundament State & Config | 7 | 🔴 Blocker |
| 2 | Transport Discovery APIs | 10 | 🔴/🟠 Parallel |
| 3 | Device Lifecycle Hot-Reload | 8.5 | 🔴 Blocker |
| 4 | Frontend Infrastruktur | 3 | 🔴 Vor Wizard |
| 5 | Wizard UI | 8.5 | 🔴 Ziel |
| 6 | Device Management UI | 2.5 | 🟠 Danach |
| Gesamt | ~42 Tage |
MVP (nur 🔴 Muss-Tasks): ~28 Tage
Backlog: Wichtige offene Punkte (kein Wizard-Blocker)
Diese Punkte wurden in der Analyse identifiziert, blockieren den Wizard nicht, sollten aber mittelfristig angegangen werden.
B.1 Authentifizierung
Priorität: 🟠 Soll (vor erstem Mehrbenutzerbetrieb)
Aktuell hat ionpy kein Auth-System. Für Einzelbetrieb im internen Netz ok. Für Laborumgebung mit mehreren Personen Pflicht.
Empfohlener Ansatz: Einfaches API-Key System
# config.yaml Erweiterung: # auth: # enabled: false # api_key: "mein-geheimer-key-hier" # web/auth.py (NEU): from fastapi import Security, HTTPException from fastapi.security.api_key import APIKeyHeader API_KEY_NAME = "X-API-Key" api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) async def verify_api_key(api_key: str = Security(api_key_header)): if not AUTH_ENABLED: return True # Auth deaktiviert → alles durch if api_key == CONFIGURED_API_KEY: return True raise HTTPException(status_code=403, detail="Ungültiger API Key") # Dann in Endpoints: # @router.post("/control") # async def send_control(..., _=Depends(verify_api_key)):
Frontend:
// api.js anpassen: const API_KEY = localStorage.getItem('ionpy_api_key') || ''; // Header bei jedem Request mitschicken headers: { 'X-API-Key': API_KEY, ... }
Mittelfristig: JWT-Token mit Login-Seite.
B.2 Path-Traversal Fix Workspaces
Priorität: 🔴 Muss (Sicherheitslücke!)
# AKTUELL - unsicher: @router.get("/workspaces/{name}") def api_load_workspace(name: str): filepath = os.path.join(WORKSPACE_DIR, f"{name}.json") # name = "../../config" würde config.yaml lesen! # FIX: import pathlib @router.get("/workspaces/{name}") def api_load_workspace(name: str): # Nur alphanumerisch + Bindestriche erlauben if not name.replace('-', '').replace('_', '').isalnum(): raise HTTPException(400, "Ungültiger Workspace-Name") base = pathlib.Path(WORKSPACE_DIR).resolve() target = (base / f"{name}.json").resolve() # Sicherstellen dass target wirklich in base liegt if not str(target).startswith(str(base)): raise HTTPException(400, "Ungültiger Pfad") if not target.exists(): raise HTTPException(404, "Workspace nicht gefunden") return json.loads(target.read_text(encoding='utf-8'))
B.3 Alarm-System Ende-zu-Ende
Priorität: 🟠 Soll
Backend erkennt Alarme bereits (HIHI/HI/LO/LOLO in
NumericEntity._evaluate_alarms()), aber:
- Keine Alarm-Übersicht im UI
- Kein visuelles/akustisches Signal bei neuem Alarm
- Keine Alarm-Quittierung
- Keine Alarm-Historie
Vorgeschlagenes Vorgehen:
- Alarm-Event über SystemEvent-Mechanismus publishen
- Alarm-Badge im Header (Zahl aktiver Alarme)
- Alarm-Panel Widget (Liste aller aktiven + historischen Alarme)
- Optional: Browser Notification API für aktive Tabs
B.4 Graceful Shutdown
Priorität: 🟠 Soll
# core/engine.py - stop() erweitern: async def stop(self): self._running = False logger.info("Engine fährt herunter...") # 1. State sichern await self.state_manager.save_now() # 2. Alle Geräte sauber trennen disconnect_tasks = [] for dev in self.device_manager.devices.values(): disconnect_tasks.append(self._disconnect_safe(dev)) if disconnect_tasks: await asyncio.gather(*disconnect_tasks, return_exceptions=True) logger.info("Alle Geräte getrennt. Engine gestoppt.") async def _disconnect_safe(self, device): try: await asyncio.wait_for(device._do_disconnect(), timeout=5.0) except Exception as e: logger.warning(f"[{device.device_id}] Disconnect Fehler: {e}")
B.5 Audit-Trail für Steuerbefehle
Priorität: 🟡 Kann
Wer hat wann welchen Befehl gesendet? Wichtig sobald mehrere Personen das System bedienen.
# core/audit_logger.py (NEU) - einfaches CSV/JSON Log: # Timestamp | User/IP | Device | Entity | OldValue | NewValue # In web/rest.py bei /control: # audit_logger.log(request.client.host, cmd.device_id, # cmd.key, cmd.value)
B.6 Unit Tests
Priorität: 🟠 Soll
# tests/conftest.py import pytest from core.engine import SystemEngine @pytest.fixture async def engine(): eng = SystemEngine(config_path="tests/test_config.yaml") eng.start() await eng.run_hardware() yield eng await eng.stop() # tests/test_config.yaml - nur virtuelle Geräte: # devices: # - id: V01_Test # driver: V01_Battery # transport: {type: virtual} # framing: {type: none} # tests/test_device_lifecycle.py async def test_add_remove_device(engine): conf = {"id": "TEST_DEVICE", "driver": "V01_Battery", ...} await engine.device_manager.add_device(conf) assert "TEST_DEVICE" in engine.device_manager.devices await engine.device_manager.remove_device("TEST_DEVICE") assert "TEST_DEVICE" not in engine.device_manager.devices
B.7 Mobile App (PWA)
Priorität: 🟡 Kann
Separate Einstiegsseite /mobile mit:
- Bottom-Tab-Navigation
- Geräteübersicht mit Key-Metrics
- Live-Werte pro Gerät
- NOTAUS Button immer sichtbar
- Alarm-Anzeige
- PWA Manifest für Homescreen-Installation
Wiederverwendet: store.js, WebSocket, uPlot, alle Vue Utilities.
Neue Datei: static/mobile.html + static/mobile/ Verzeichnis.
B.8 Datenbank-Anbindung für Historien-Charts
Priorität: 🟠 Soll (Chart-Historie ist als “bald” markiert)
Optionen:
- SQLite – kein Setup, gut für Einzelbetrieb
- InfluxDB – optimiert für Zeitreihen, skalierbar
- TimescaleDB – PostgreSQL-Extension, guter Mittelweg
Empfehlung für ionpy: SQLite als erstes, InfluxDB als Option.
# Minimale SQLite Integration: # - Neuer Subscriber auf EventBus # - Schreibt NumericSamples in SQLite # - REST Endpoint: GET /api/history/{device}/{entity}?from=&to= # - Chart-View schaltet von "Live-Puffer" auf "DB-Abfrage" um
B.9 Config YAML Kommentare erhalten
Priorität: 🟡 Kann
Standard yaml.dump() löscht alle Kommentare. Falls der User
seine config.yaml manuell kommentiert hat, gehen diese beim
ersten Wizard-Speichern verloren.
Fix: ruamel.yaml statt pyyaml für das Schreiben verwenden.
from ruamel.yaml import YAML yaml = YAML() yaml.preserve_quotes = True # Dann yaml.dump(data, file) statt yaml.dump(data, file)
B.10 Mehrsprachigkeit (i18n)
Priorität: 🟡 Kann
Aktuell komplett Deutsch hardcoded. Für internationale Nutzung oder gemischte Teams relevant.
Vue 3 i18n Plugin + JSON-Sprachdateien.
Aufwand: ~5 Tage initial, danach Übersetzungen hinzufügen.
B.11 Error Boundaries im Frontend
Priorität: 🟠 Soll
Wenn eine Vue-Komponente wirft, reißt sie aktuell das gesamte
Widget mit. errorCaptured-Hook fehlt.
// In spawn() nach dem createApp(): vueApp.config.errorHandler = (err, instance, info) => { console.error(`Widget Fehler [${wid}]:`, err, info); // Widget mit Fehlermeldung ersetzen statt alles zu crashen const body = document.getElementById(wid); if (body) body.innerHTML = ` <div style="padding: 20px; color: #ef4444; font-size: 12px;"> <i class="mdi mdi-alert-circle"></i> Widget Fehler<br> <small style="color: #71717a;">${err.message}</small> </div> `; };
B.12 Dokumentation
Priorität: 🟠 Soll
- Treiber-Entwicklerdoku: Wie schreibt man einen neuen Treiber?
- API-Doku: FastAPI
'/docs' ist bereits vorhanden (Swagger UI) - Deployment-Guide: Systemd-Service, Autostart
- config.yaml Referenz mit allen Optionen
Ende der Roadmap
Erstellt im Kontext der ionpy Pro OS Implementierungsdiskussion
Alle Code-Beispiele sind Implementierungshinweise, keine fertigen Lösungen
