# ============================= # app/simulation/modules/basic.py # ============================= from __future__ import annotations from app.simulation.simulator import Module, Vehicle import bisect def _ocv_from_soc(soc: float, table: dict[float, float]) -> float: # table: {SOC: OCV} unsortiert → linear interpolieren xs = sorted(table.keys()) ys = [table[x] for x in xs] s = max(0.0, min(1.0, soc)) i = bisect.bisect_left(xs, s) if i <= 0: return ys[0] if i >= len(xs): return ys[-1] x0, x1 = xs[i-1], xs[i] y0, y1 = ys[i-1], ys[i] t = 0.0 if x1 == x0 else (s - x0) / (x1 - x0) return y0 + t*(y1 - y0) class BasicModule(Module): PRIO = 10 NAME = "basic" """ - Zündungslogik inkl. START→ON nach crank_time_s - Ambient-Temperatur als globale Umweltgröße - Elektrik: * Load/Source-Aggregation via Vehicle-Helpers * Lichtmaschine drehzahlabhängig, Regler auf alternator_reg_v * Batterie: Kapazität (Ah), Innenwiderstand, OCV(SOC); I_batt > 0 => Entladung """ def __init__(self): self.crank_time_s = 2.7 self._crank_timer = 0.0 def apply(self, v: Vehicle, dt: float) -> None: # ----- Dashboard registration (unverändert) ----- v.register_metric("ignition", label="Zündung", source="basic", priority=5) v.register_metric("ambient_c", label="Umgebung", unit="°C", fmt=".1f", source="basic", priority=7) v.register_metric("battery_voltage", label="Batteriespannung", unit="V", fmt=".2f", source="basic", priority=8) v.register_metric("elx_voltage", label="ELX-Spannung", unit="V", fmt=".2f", source="basic", priority=10) v.register_metric("system_voltage", label="Systemspannung", unit="V", fmt=".2f", source="basic", priority=11) v.register_metric("battery_soc", label="Batterie SOC", unit="", fmt=".2f", source="basic", priority=12) v.register_metric("battery_current_a", label="Batterie Strom", unit="A", fmt=".2f", source="basic", priority=13) v.register_metric("alternator_current_a", label="Lima Strom", unit="A", fmt=".2f", source="basic", priority=14) v.register_metric("elec_load_total_a", label="Verbrauch ges.", unit="A", fmt=".2f", source="basic", priority=15) # ----- Read config/state ----- econf = v.config.get("electrical", {}) alt_reg_v = float(econf.get("alternator_reg_v", 14.2)) alt_rated_a = float(econf.get("alternator_rated_a", 20.0)) alt_cut_in = int(econf.get("alt_cut_in_rpm", 1500)) alt_full = int(econf.get("alt_full_rpm", 4000)) batt_cap_ah = float(econf.get("battery_capacity_ah", 8.0)) batt_rint = float(econf.get("battery_r_int_ohm", 0.020)) batt_ocv_tbl= dict(econf.get("battery_ocv_v", {})) or { 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 } ign = v.ensure("ignition", "ON") rpm = float(v.ensure("rpm", 1200)) soc = float(v.ensure("battery_soc", 0.80)) v.set("ambient_c", float(v.ensure("ambient_c", v.get("ambient_c", 20.0)))) # ----- START auto-fall to ON ----- if ign == "START": if self._crank_timer <= 0.0: self._crank_timer = float(self.crank_time_s) else: self._crank_timer -= dt if self._crank_timer <= 0.0: v.set("ignition", "ON") ign = "ON" else: self._crank_timer = 0.0 # ----- Früh-Exit: OFF/ACC -> Bus AUS, Batterie „ruht“ ----- if ign in ("OFF", "ACC"): ocv = _ocv_from_soc(soc, batt_ocv_tbl) # Batterie entspannt sich langsam gegen OCV (optional, super simpel): # (man kann hier auch gar nichts tun; ich halte batt_v = ocv für okay) batt_v = ocv v.set("battery_voltage", round(batt_v, 2)) v.set("elx_voltage", 0.0) v.set("system_voltage", 0.0) v.set("battery_current_a", 0.0) v.set("alternator_current_a", 0.0) v.set("elec_load_total_a", 0.0) v.set("battery_soc", round(soc, 3)) return # ----- ON/START: Elektrik-Bilanz ----- # Beiträge anderer Module summieren loads_a, sources_a = v.elec_totals() # Grundlasten (z.B. ECU, Relais) base_load = 0.5 if ign == "ON" else 0.6 # START leicht höher loads_a += base_load # Quellen anderer Module (z.B. DC-DC) können sources_a > 0 machen # Wir ziehen Quellen von der Last ab – was übrig bleibt, muss Lima/Batterie liefern net_load_a = max(0.0, loads_a - sources_a) # Lima-Fähigkeit aus rpm if rpm >= alt_cut_in: frac = 0.0 if rpm <= alt_cut_in else (rpm - alt_cut_in) / max(1, (alt_full - alt_cut_in)) frac = max(0.0, min(1.0, frac)) alt_cap_a = alt_rated_a * frac else: alt_cap_a = 0.0 # Batterie-OCV ocv = _ocv_from_soc(soc, batt_ocv_tbl) # Ziel: Regler hält alt_reg_v – aber nur, wenn die Lima überhaupt aktiv ist desired_charge_a = max(0.0, (alt_reg_v - ocv) / max(1e-4, batt_rint)) if alt_cap_a > 0.0 else 0.0 alt_needed_a = net_load_a + desired_charge_a alt_i = min(alt_needed_a, alt_cap_a) # Batterie-Bilanz if alt_cap_a > 0.0 and alt_i >= net_load_a: # Lima deckt alles; Überschuss lädt Batterie batt_i = -(alt_i - net_load_a) # negativ = lädt bus_v = alt_reg_v else: # Lima (falls vorhanden) reicht nicht -> Batterie liefert Defizit deficit = net_load_a - alt_i batt_i = max(0.0, deficit) # positiv = entlädt bus_v = ocv - batt_i * batt_rint # SOC-Update (Ah-Bilanz) soc = max(0.0, min(1.0, soc - (batt_i * dt) / (3600.0 * max(0.1, batt_cap_ah)))) batt_v = ocv - (batt_i * batt_rint) # Klammern/Spiegeln batt_v = max(10.0, min(15.5, batt_v)) bus_v = max(0.0, min(15.5, bus_v)) v.set("battery_voltage", round(batt_v, 2)) v.set("elx_voltage", round(bus_v, 2)) v.set("system_voltage", round(bus_v, 2)) v.set("battery_soc", round(soc, 3)) v.set("battery_current_a", round(batt_i, 2)) v.set("alternator_current_a", round(min(alt_i, alt_cap_a), 2)) v.set("elec_load_total_a", round(net_load_a, 2))