# app/simulation/vehicle.py from __future__ import annotations from dataclasses import dataclass, field from typing import Dict, Any, List @dataclass class Vehicle: """Dynamic property-bag vehicle.""" state: Dict[str, Any] = field(default_factory=lambda: { "rpm": 1400, "speed_kmh": 0.0, "gear": 0, "throttle_pct": 0, "ignition": "OFF", # elektrische Live-Werte "battery_voltage": 12.6, # Batterie-Klemmenspannung "elx_voltage": 0.0, # Bordnetz/Bus-Spannung "system_voltage": 12.4, # alias "battery_soc": 0.80, # 0..1 "battery_current_a": 0.0, # + entlädt, – lädt "alternator_current_a": 0.0, # von Lima geliefert "elec_load_total_a": 0.0, # Summe aller Verbraucher "ambient_c": 20.0, }) config: Dict[str, Any] = field(default_factory=lambda: { "vehicle": { "type": "motorcycle", "mass_kg": 210.0, "abs": True, "tcs": False, }, # Elektrik-Parameter (global) "electrical": { "battery_capacity_ah": 8.0, "battery_r_int_ohm": 0.020, # ~20 mΩ # sehr einfache OCV(SOC)-Kennlinie "battery_ocv_v": { # bei ~20°C 0.0: 11.8, 0.1: 12.0, 0.2: 12.1, 0.3: 12.2, 0.4: 12.3, 0.5: 12.45, 0.6: 12.55, 0.7: 12.65, 0.8: 12.75, 0.9: 12.85, 1.0: 12.95 }, "alternator_reg_v": 14.2, "alternator_rated_a": 20.0, # Nennstrom "alt_cut_in_rpm": 1500, # ab hier fängt sie an zu liefern "alt_full_rpm": 4000, # ab hier volle Kapazität }, }) dtc: Dict[str, bool] = field(default_factory=dict) dashboard_specs: Dict[str, Dict[str, Any]] = field(default_factory=dict) # accumulator für dieses Sim-Frame _elec_loads_a: Dict[str, float] = field(default_factory=dict) _elec_sources_a: Dict[str, float] = field(default_factory=dict) # ---- helpers for modules ---- def get(self, key: str, default: Any = None) -> Any: return self.state.get(key, default) def set(self, key: str, value: Any) -> None: self.state[key] = value def ensure(self, key: str, default: Any) -> Any: return self.state.setdefault(key, default) # Dashboard registry (wie gehabt) def register_metric(self, key: str, *, label: str | None = None, unit: str | None = None, fmt: str | None = None, source: str | None = None, priority: int = 100, overwrite: bool = False) -> None: spec = self.dashboard_specs.get(key) if spec and not overwrite: if label and not spec.get("label"): spec["label"] = label if unit and not spec.get("unit"): spec["unit"] = unit if fmt and not spec.get("fmt"): spec["fmt"] = fmt if source and not spec.get("source"): spec["source"] = source if spec.get("priority") is None: spec["priority"] = priority return self.dashboard_specs[key] = { "key": key, "label": label or key, "unit": unit, "fmt": fmt, "source": source, "priority": priority, } def dashboard_snapshot(self) -> Dict[str, Any]: return {"specs": dict(self.dashboard_specs), "values": dict(self.state)} def snapshot(self) -> Dict[str, Any]: return dict(self.state) # ---- Electrical frame helpers ---- def elec_reset_frame(self) -> None: self._elec_loads_a.clear() self._elec_sources_a.clear() def elec_add_load(self, name: str, amps: float) -> None: # positive Werte = Stromaufnahme self._elec_loads_a[name] = max(0.0, float(amps)) def elec_add_source(self, name: str, amps: float) -> None: # positive Werte = Einspeisung self._elec_sources_a[name] = max(0.0, float(amps)) def elec_totals(self) -> tuple[float, float]: return sum(self._elec_loads_a.values()), sum(self._elec_sources_a.values()) class Module: def apply(self, v: Vehicle, dt: float) -> None: pass class Orchestrator: def __init__(self, vehicle: Vehicle): self.vehicle = vehicle self.modules: List[Module] = [] def add(self, m: Module): self.modules.append(m) def update(self, dt: float): # Pro Frame die Electrical-Recorder nullen self.vehicle.elec_reset_frame() for m in self.modules: m.apply(self.vehicle, dt)