# -*- coding: utf-8 -*- # Absichtlich ohne Shebang: der Windows-py-Launcher wuerde daraus "python3" # ableiten und scheitert, wenn keine PythonCore-Installation registriert ist. """ e_planning.py - Bruecke Layout <-> E-Planung ueber die Symbolpositionen. Das Layout (CSV) sagt, welche Separatoren, Weichen, Aus-/Einschleuselemente es gibt und wo; die E-Planung, wie ihre Sensoren und Ventile heissen (BG0642, MB0643 ...). Die EA-Liste allein kennt keine Topologie. Die E-Planung wird aber als Layer ueber der Hardwareplanung gezeichnet: jedes Sensor-/Aktorsymbol liegt neben dem Bauteil, zu dem es gehoert. Das Kabellaengen-Tool (kabellaengen/lib/getpositions.py) schreibt diese Symbole mit Position und Attributen nach _positionsdraw.json. Hier wird daraus die Zuordnung Layout-Bauteil -> BMK: 1. E-Bauteile einlesen (`sensors` und `schaltschrank_elemente`). 2. Lagebezug E-Zeichnung -> CSV mit lib/dxf_registration.py; Anker sind die Separator-Sensoren der E-Planung gegen die Separatoren des CSV. Der gepruefte Bezug wird in SKEL_CFG/dxf_registration.json abgelegt. 3. Regeln aus SKEL_CFG/e_planning.ini anwenden, je Rolle (z. B. "Separator.sensor"): welche Layout-Art, welche E-Verwendung (VERW), welcher Hoechstabstand - oder "Nummer = Nummer des Partners + Versatz" (Ventil MB0643 zum Sensor BG0642). Die Zuordnung ist eindeutig: jedes E-Bauteil und jede Rolle eines Layout-Bauteils wird hoechstens einmal vergeben, die kuerzesten Abstaende zuerst. 4. Ergebnis als _eplan.json (EplanDocument) und Bericht _eplan.md nach SKEL_RESULTS. scl_skeleton.py --eplan liest die JSON und setzt die gefundenen Namen in die SCL ein (SclName mit todo=False). Was keine Regel trifft, bleibt dort TODO(E-Planung) - geraten wird nicht. Schichten: e_planning importiert material_flow (CSV) und dxf_registration (Lagebezug); scl_context importiert nur das Modell EplanDocument. Aufruf ueber bin/e_planning.bat bzw. bin/e_planning.sh. """ from __future__ import annotations import argparse import configparser import fnmatch import json import math import re import sys from dataclasses import dataclass, field from datetime import datetime from pathlib import Path from pydantic import BaseModel, ConfigDict, Field from dxf_registration import ( RegistrationError, Transform, derive_transform, load_transform, save_transform, verify_transform, ) from parameter import get_float __all__ = [ "EPLAN_SCHEMA", "EDevice", "LayoutPoint", "Rule", "Assignment", "Drive", "EplanDocument", "load_positionsdraw", "load_rules", "assign", "pair_drives", ] EPLAN_SCHEMA = "sps_skel-eplan-v1" RULES_FILE = "e_planning.ini" # Zulaessige Abweichung vom Sollabstand, wenn eine Regel kein toleranz_mm # angibt - aus cfg/parameter.ini [e_planning] (lib/parameter.py). DEFAULT_TOLERANCE_MM = get_float("e_planning", "toleranz_mm") _CONFIG = ConfigDict(extra="forbid") # "=A1+UH01-KF107" -> Ort/Steuerung "UH01", Geraet "KF107" _KENNZEICHNUNG = re.compile(r"\+([A-Za-z0-9]+)-([A-Za-z0-9]+)") # "MB0643" -> ("MB", "0643") _BMK = re.compile(r"^([A-Za-z]+)(\d+)$") # --------------------------------------------------------------------------- # Modell # --------------------------------------------------------------------------- class EDevice(BaseModel): """Ein Sensor/Aktor/Schrankelement der E-Planung.""" model_config = _CONFIG bmk: str verw: str = "" bezeichnung: str = "" kennzeichnung: str = "" # Position in der E-Zeichnung (mm); None bei Schrankelementen ohne Lage pos: tuple[float, float] | None = None # True: aus "schaltschrank_elemente" (im Schrank, nicht im Feld) cabinet: bool = False @property def controller(self) -> str: """Steuerung/Ort aus der Kennzeichnung ("UH01"), "" wenn unbekannt.""" m = _KENNZEICHNUNG.search(self.kennzeichnung) return m.group(1) if m else "" @property def station(self) -> str: """E/A-Station aus der Kennzeichnung ("KF107"), "" wenn unbekannt.""" m = _KENNZEICHNUNG.search(self.kennzeichnung) return m.group(2) if m else "" @property def prefix(self) -> str: m = _BMK.match(self.bmk) return m.group(1) if m else "" @property def number(self) -> int | None: m = _BMK.match(self.bmk) return int(m.group(2)) if m else None @dataclass class LayoutPoint: """Ein punktartiges Bauteil des Layouts in CSV-Koordinaten.""" layout_id: str kind: str x: float y: float name: str = "" @dataclass class Rule: """Eine Zuordnungsregel aus e_planning.ini (Abschnitt ".").""" role: str # "Separator.sensor" layout: list[str] # Muster fuer die Layout-Art verw: list[str] # Muster fuer VERW max_mm: float | None = None # raeumlich: Hoechstabstand number_from: str = "" # Nummernregel: Rolle des Partners ... number_offset: int = 0 # ... und Versatz der Nummer prefix: str = "" # BMK-Praefix, leer = egal # Sollabstand: das Symbol liegt immer gleich weit neben dem Bauteil. # Dann zaehlt die Abweichung vom Soll, nicht der kleinste Abstand. target_mm: float | None = None tolerance_mm: float = DEFAULT_TOLERANCE_MM def score(self, dist: float) -> float | None: """Rangfolge eines Kandidaten (kleiner = besser), None = nicht zulaessig.""" if self.max_mm is not None and dist > self.max_mm: return None if self.target_mm is None: return dist dev = abs(dist - self.target_mm) return dev if dev <= self.tolerance_mm else None class Assignment(BaseModel): """Ein E-Bauteil in einer Rolle an einem Layout-Bauteil.""" model_config = _CONFIG layout_id: str layout_kind: str role: str bmk: str verw: str = "" bezeichnung: str = "" controller: str = "" station: str = "" # Abstand nach dem Lagebezug in mm (auch bei der Nummernregel zur Kontrolle) distance: float # "raeumlich" | "nummer" rule: str # Abstand des naechstbesten Kandidaten derselben Rolle (None: keiner) runner_up: float | None = None class Drive(BaseModel): """Motor im Feld und sein Motorschutz im Schrank (gleiche Nummer).""" model_config = _CONFIG motor: str protection: str = "" bezeichnung: str = "" protection_bezeichnung: str = "" class EplanDocument(BaseModel): """Die Zuordnungs-JSON _eplan.json.""" model_config = ConfigDict(extra="forbid", populate_by_name=True) schema_id: str = Field(default=EPLAN_SCHEMA, alias="schema") created: str = "" csv_file: str = "" eplan_file: str = "" transform: Transform | None = None assignments: list[Assignment] = Field(default_factory=list) drives: list[Drive] = Field(default_factory=list) # E-Bauteile im Feld ohne Zuordnung (BMK) unassigned: list[str] = Field(default_factory=list) warnings: list[str] = Field(default_factory=list) def for_layout(self, layout_id: str) -> dict[str, Assignment]: """Rollen eines Layout-Bauteils: {"sensor": Assignment, ...}.""" return { a.role.split(".", 1)[-1]: a for a in self.assignments if a.layout_id == layout_id } def to_json(self) -> str: return self.model_dump_json(by_alias=True, indent=2) + "\n" @classmethod def from_json(cls, text: str) -> "EplanDocument": return cls.model_validate_json(text) # --------------------------------------------------------------------------- # Einlesen # --------------------------------------------------------------------------- def _device(key: str, raw: dict, cabinet: bool) -> EDevice: # Schluessel "BG0642@UNKNOWN" / "QA7000@A/B": vor dem @ steht das BMK. bmk = str(raw.get("IO") or raw.get("B") or key.split("@", 1)[0]).strip() pos = raw.get("pos") return EDevice( bmk=bmk, verw=str(raw.get("VERW") or "").strip(), bezeichnung=str(raw.get("BEZEICHNUNG") or "").strip(), kennzeichnung=str(raw.get("KENNZEICHNUNG") or "").strip(), pos=(float(pos[0]), float(pos[1])) if pos and len(pos) >= 2 else None, cabinet=cabinet, ) def load_positionsdraw(path: Path, warnings: list[str] | None = None) -> list[EDevice]: """E-Bauteile aus einer *_positionsdraw.json lesen. Ein BMK kommt hoechstens einmal vor. Steht es doppelt da (im Schrank einmal mit und einmal ohne Attribute), gewinnt der Eintrag mit VERW. """ warnings = warnings if warnings is not None else [] data = json.loads(path.read_text(encoding="utf-8")) devices: dict[str, EDevice] = {} for section, cabinet in (("sensors", False), ("schaltschrank_elemente", True)): for key, raw in (data.get(section) or {}).items(): if not isinstance(raw, dict): continue dev = _device(key, raw, cabinet) if not dev.bmk: continue old = devices.get(dev.bmk) if old is None or (not old.verw and dev.verw): devices[dev.bmk] = dev for bmk, positions in (data.get("double_ids") or {}).items(): warnings.append(f"E-Planung: {bmk} steht {len(positions)}x in der Zeichnung " f"- verwendet wird ein Eintrag") return list(devices.values()) def _patterns(text: str) -> list[str]: return [p.strip() for p in text.split(",") if p.strip()] def load_rules(path: Path) -> tuple[list[Rule], list[str]]: """Regeln und Anker-Verwendungen aus e_planning.ini lesen. Abschnitt [Registrierung] -> anker (VERW-Muster fuer den Lagebezug); jeder Abschnitt "." ist eine Regel, in Dateireihenfolge. """ parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"), interpolation=None) parser.optionxform = str # Schluessel nicht kleinschreiben with path.open(encoding="utf-8") as fh: parser.read_file(fh) anchors = _patterns(parser.get("Registrierung", "anker", fallback="IN SEP")) rules = [] for section in parser.sections(): if "." not in section: continue s = parser[section] max_mm = s.get("max_mm") soll = s.get("soll_mm") rules.append(Rule( role=section, layout=_patterns(s.get("layout", section.split(".", 1)[0])), verw=_patterns(s.get("verw", "*")), max_mm=float(max_mm) if max_mm else None, number_from=s.get("nummer_von", "").strip(), number_offset=int(s.get("nummer_versatz", "0")), prefix=s.get("praefix", "").strip(), target_mm=float(soll) if soll else None, tolerance_mm=float(s.get("toleranz_mm", str(DEFAULT_TOLERANCE_MM))), )) known = {r.role for r in rules} for r in rules: if r.number_from and r.number_from not in known: raise ValueError(f"{path.name} [{r.role}]: nummer_von = {r.number_from} " f"ist keine Regel") return rules, anchors def _match(text: str, patterns: list[str]) -> bool: return any(fnmatch.fnmatchcase(text.upper(), p.upper()) for p in patterns) # --------------------------------------------------------------------------- # Zuordnung # --------------------------------------------------------------------------- @dataclass class _State: positions: dict[str, tuple[float, float]] used: set[str] = field(default_factory=set) result: list[Assignment] = field(default_factory=list) # (layout_id, role) -> Assignment by_slot: dict[tuple[str, str], Assignment] = field(default_factory=dict) def _make(point: LayoutPoint, rule: Rule, dev: EDevice, dist: float, how: str, runner_up: float | None) -> Assignment: return Assignment( layout_id=point.layout_id, layout_kind=point.kind, role=rule.role, bmk=dev.bmk, verw=dev.verw, bezeichnung=dev.bezeichnung, controller=dev.controller, station=dev.station, distance=round(dist, 1), rule=how, runner_up=None if runner_up is None else round(runner_up, 1), ) def _candidates(rule: Rule, devices: list[EDevice], state: _State) -> list[EDevice]: return [ d for d in devices if d.bmk not in state.used and d.bmk in state.positions and _match(d.verw, rule.verw) and (not rule.prefix or d.prefix.upper() == rule.prefix.upper()) ] def _assign_spatial(rule: Rule, points: list[LayoutPoint], devices: list[EDevice], state: _State) -> None: """Beste Bewertung zuerst (Rule.score); je Bauteil und E-Bauteil hoechstens einmal.""" cands = _candidates(rule, devices, state) pairs = [] for p in points: if (p.layout_id, rule.role) in state.by_slot: continue for d in cands: dist = math.dist((p.x, p.y), state.positions[d.bmk]) score = rule.score(dist) if score is not None: pairs.append((score, dist, p.layout_id, d.bmk)) pairs.sort() # je Bauteil die Kandidaten in Rangfolge - fuer "naechstbester Kandidat" by_point: dict[str, list[tuple[str, float]]] = {} for _, dist, pid, bmk in pairs: by_point.setdefault(pid, []).append((bmk, dist)) point_of = {p.layout_id: p for p in points} dev_of = {d.bmk: d for d in cands} for _, dist, pid, bmk in pairs: if (pid, rule.role) in state.by_slot or bmk in state.used: continue others = [x for b, x in by_point[pid] if b != bmk] a = _make(point_of[pid], rule, dev_of[bmk], dist, "raeumlich", others[0] if others else None) state.used.add(bmk) state.by_slot[(pid, rule.role)] = a state.result.append(a) def _assign_by_number(rule: Rule, points: list[LayoutPoint], devices: list[EDevice], state: _State) -> None: """Nummer = Nummer des Partners (Rolle nummer_von) + Versatz.""" cands = {(d.number): d for d in _candidates(rule, devices, state) if d.number is not None} for p in points: if (p.layout_id, rule.role) in state.by_slot: continue partner = state.by_slot.get((p.layout_id, rule.number_from)) if partner is None: continue m = _BMK.match(partner.bmk) if not m: continue dev = cands.get(int(m.group(2)) + rule.number_offset) if dev is None or dev.bmk in state.used: continue dist = math.dist((p.x, p.y), state.positions[dev.bmk]) # Auch die Nummernregel darf nicht quer ueber die Anlage greifen if rule.max_mm is not None and dist > rule.max_mm: continue a = _make(p, rule, dev, dist, "nummer", None) state.used.add(dev.bmk) state.by_slot[(p.layout_id, rule.role)] = a state.result.append(a) def assign(points: list[LayoutPoint], devices: list[EDevice], rules: list[Rule], transform: Transform) -> list[Assignment]: """Regeln der Reihe nach anwenden. Eine Regel mit nummer_von versucht zuerst die Nummernregel und nimmt fuer die verbleibenden Bauteile den Abstand (wenn max_mm/soll_mm gesetzt ist). So wird das Ventil eines Separators ueber die Nummer gefunden, auch wenn das Ventil des Nachbarn naeher liegt (Speicherlinien im 800-mm-Raster). """ state = _State(positions={ d.bmk: transform.to_csv(*d.pos) for d in devices if d.pos is not None and not d.cabinet }) for rule in rules: targets = [p for p in points if _match(p.kind, rule.layout)] if rule.number_from: _assign_by_number(rule, targets, devices, state) if rule.max_mm is not None or rule.target_mm is not None: _assign_spatial(rule, targets, devices, state) return sorted(state.result, key=lambda a: (a.layout_id, a.role)) def pair_drives(devices: list[EDevice]) -> list[Drive]: """Motor MAnnnn <-> Motorschutz FCnnnn (gleiche Nummer).""" fc = {d.number: d for d in devices if d.prefix.upper() == "FC" and d.number is not None} drives = [] for d in sorted(devices, key=lambda d: d.bmk): if d.prefix.upper() != "MA" or d.number is None: continue schutz = fc.get(d.number) drives.append(Drive( motor=d.bmk, protection=schutz.bmk if schutz else "", bezeichnung=d.bezeichnung, protection_bezeichnung=schutz.bezeichnung if schutz else "", )) return drives # --------------------------------------------------------------------------- # Layout und Lagebezug # --------------------------------------------------------------------------- # Element.kind der punktartigen Layout-Bauteile LAYOUT_KINDS = ("Separator", "Scanner", "Weiche", "Ausschleus", "Einschleus", "Beladung", "Entladung") def layout_points(elements) -> list[LayoutPoint]: """Punktartige Bauteile aus den CSV-Elementen (material_flow.Element).""" points = [] for e in elements: if e.position is None: continue kind = next((k for k in LAYOUT_KINDS if k.lower() in f"{e.kind} {e.teile_art}".lower()), "") if kind: points.append(LayoutPoint(e.teile_id, kind, e.x, e.y, e.name)) return points def _anchor_points(points: list[LayoutPoint], devices: list[EDevice], anchors: list[str]): csv_pts = {"Separator": [(p.x, p.y) for p in points if p.kind == "Separator"]} e_pts = {"Separator": [d.pos for d in devices if d.pos is not None and not d.cabinet and _match(d.verw, anchors)]} return csv_pts, e_pts def resolve_transform(points, devices, anchors, cfg_dir: Path, csv_name: str, eplan_name: str, re_register: bool, warnings: list[str]) -> Transform: """Abgelegten Lagebezug nachrechnen oder neu ermitteln und ablegen.""" csv_pts, e_pts = _anchor_points(points, devices, anchors) if not re_register: stored = load_transform(cfg_dir, csv_name, eplan_name) if stored: verify_transform(stored, csv_pts, e_pts) return stored transform = derive_transform(csv_pts, e_pts, csv_name, eplan_name) saved = save_transform(cfg_dir, transform) warnings.append(f"Lagebezug neu ermittelt und abgelegt: {saved}") return transform # --------------------------------------------------------------------------- # Bericht # --------------------------------------------------------------------------- def render_report(doc: EplanDocument, rules: list[Rule], points: list[LayoutPoint], devices: list[EDevice]) -> str: out = [] add = out.append add(f"# E-Planung -> Layout: {doc.csv_file}") add("") add(f"- E-Planung: `{doc.eplan_file}`") add(f"- Erzeugt: {doc.created}") if doc.transform: add(f"- Lagebezug: {doc.transform.describe()}") add("") add("## Zuordnung je Rolle") add("") add("| Rolle | Layout-Bauteile | zugeordnet | davon Nummernregel |") add("|---|---|---|---|") for r in rules: n_pts = sum(1 for p in points if _match(p.kind, r.layout)) hits = [a for a in doc.assignments if a.role == r.role] n_num = sum(1 for a in hits if a.rule == "nummer") add(f"| {r.role} | {n_pts} | {len(hits)} | {n_num} |") add("") add("## Zuordnungen") add("") add("Abstand nach dem Lagebezug; \"2.\" = Abstand des naechstbesten Kandidaten " "(knapp beieinander = pruefen).") add("") add("| Layout | Art | Rolle | BMK | VERW | Bezeichnung | Abstand mm | 2. | Regel | Station |") add("|---|---|---|---|---|---|---|---|---|---|") for a in doc.assignments: second = "" if a.runner_up is None else f"{a.runner_up:.0f}" add(f"| {a.layout_id} | {a.layout_kind} | {a.role.split('.', 1)[-1]} | {a.bmk} " f"| {a.verw} | {a.bezeichnung} | {a.distance:.0f} | {second} | {a.rule} " f"| {a.controller}-{a.station} |") add("") roles_of: dict[str, set[str]] = {} for a in doc.assignments: roles_of.setdefault(a.layout_id, set()).add(a.role) add("## Layout-Bauteile ohne Zuordnung") add("") any_missing = False for r in rules: missing = [p for p in points if _match(p.kind, r.layout) and r.role not in roles_of.get(p.layout_id, set())] if missing: any_missing = True add(f"- **{r.role}**: {', '.join(p.layout_id for p in missing)}") if not any_missing: add("- keine") add("") by_bmk = {d.bmk: d for d in devices} add("## E-Bauteile im Feld ohne Zuordnung") add("") if doc.unassigned: add("| BMK | VERW | Bezeichnung | Station |") add("|---|---|---|---|") for bmk in doc.unassigned: d = by_bmk[bmk] add(f"| {bmk} | {d.verw} | {d.bezeichnung} | {d.controller}-{d.station} |") else: add("- keine") add("") add("## Antriebe (Motor <-> Motorschutz)") add("") if doc.drives: add("| Motor | Bezeichnung | Motorschutz | Bezeichnung |") add("|---|---|---|---|") for dr in doc.drives: add(f"| {dr.motor} | {dr.bezeichnung} | {dr.protection or '-'} " f"| {dr.protection_bezeichnung} |") else: add("- keine Motoren in der E-Planung") add("") if doc.warnings: add("## Hinweise") add("") for w in doc.warnings: add(f"- {w}") add("") return "\n".join(out) # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- def build_document(csv_path: Path, eplan_path: Path, rules_path: Path, cfg_dir: Path, re_register: bool = False ) -> tuple[EplanDocument, list[Rule], list[LayoutPoint], list[EDevice]]: """Kompletter Lauf ohne Ausgabe: CSV + positionsdraw -> EplanDocument.""" from material_flow import read_elements # erst hier: Tests ohne CSV brauchen es nicht warnings: list[str] = [] elements = read_elements(csv_path, warnings) points = layout_points(elements) devices = load_positionsdraw(eplan_path, warnings) rules, anchors = load_rules(rules_path) transform = resolve_transform(points, devices, anchors, cfg_dir, csv_path.name, eplan_path.name, re_register, warnings) assignments = assign(points, devices, rules, transform) used = {a.bmk for a in assignments} unassigned = sorted(d.bmk for d in devices if not d.cabinet and d.pos is not None and d.bmk not in used) doc = EplanDocument( created=datetime.now().strftime("%Y-%m-%d %H:%M"), csv_file=csv_path.name, eplan_file=eplan_path.name, transform=transform, assignments=assignments, drives=pair_drives(devices), unassigned=unassigned, warnings=warnings, ) return doc, rules, points, devices def main(argv: list[str] | None = None) -> int: from material_flow import env_dir, resolve_input args = parse_args(argv) csv_path = resolve_input(args.file) eplan_path = resolve_input(args.eplan) cfg_dir = env_dir("SKEL_CFG", "cfg") rules_path = Path(args.rules) if args.rules else cfg_dir / RULES_FILE for label, path in (("CSV", csv_path), ("E-Planung", eplan_path), ("Regeln", rules_path)): if not path.is_file(): print(f"FEHLER: {label} nicht gefunden: {path}", file=sys.stderr) return 1 try: doc, rules, points, devices = build_document( csv_path, eplan_path, rules_path, cfg_dir, args.re_register) except RegistrationError as exc: print(f"FEHLER: Lagebezug E-Planung -> CSV nicht nachweisbar: {exc}", file=sys.stderr) print(" Passen CSV und E-Zeichnung zusammen? Anker-VERW in " f"{rules_path.name} [Registrierung] pruefen.", file=sys.stderr) return 1 except (OSError, ValueError) as exc: print(f"FEHLER: {exc}", file=sys.stderr) return 1 results = env_dir("SKEL_RESULTS", "results") results.mkdir(parents=True, exist_ok=True) json_file = results / f"{csv_path.stem}_eplan.json" md_file = results / f"{csv_path.stem}_eplan.md" json_file.write_text(doc.to_json(), encoding="utf-8") md_file.write_text(render_report(doc, rules, points, devices), encoding="utf-8") print("") print("================================================================") print("E-PLANUNG -> LAYOUT") print("================================================================") print(f"CSV = {csv_path}") print(f"E-Planung = {eplan_path}") print(f"Lagebezug = {doc.transform.describe() if doc.transform else '-'}") for r in rules: n_pts = sum(1 for p in points if _match(p.kind, r.layout)) hits = sum(1 for a in doc.assignments if a.role == r.role) print(f" {r.role:<26} {hits:4} von {n_pts}") print(f"Ohne Zuordnung = {len(doc.unassigned)} E-Bauteile im Feld") print(f"Antriebe = {len(doc.drives)} " f"({sum(1 for d in doc.drives if d.protection)} mit Motorschutz)") for w in doc.warnings: print(f" ! {w}") print(f"JSON = {json_file}") print(f"Bericht = {md_file}") print("================================================================") print("") return 0 def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( prog="e_planning", description="Ordnet die Sensoren/Ventile der E-Planung " "(*_positionsdraw.json des Kabellaengen-Tools) den Bauteilen " "des Layouts (CSV) zu. Ergebnis: _eplan.json fuer " "scl_skeleton --eplan und ein Bericht _eplan.md.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="Beispiele:\n" " e_planning.bat --file mubea.csv --eplan Mubea_positionsdraw.json\n" " e_planning.bat --file mubea.csv --eplan C:\\pfad\\Mubea_positionsdraw.json " "--re-register\n" "\n" "Exit-Codes:\n" " 0 ok (auch wenn nicht alles zugeordnet wurde - siehe Bericht)\n" " 1 Eingabe-, Aufruf- oder Lagebezugsfehler\n", ) parser.add_argument("--file", default="export.csv", metavar="NAME", help="CSV-Export in %%SKEL_DATA%% (oder ein Pfad). " "Standard: %(default)s") parser.add_argument("--eplan", required=True, metavar="NAME", help="*_positionsdraw.json der E-Planung in %%SKEL_DATA%% " "(oder ein Pfad)") parser.add_argument("--rules", metavar="DATEI", help=f"Regeldatei. Standard: %%SKEL_CFG%%\\{RULES_FILE}") parser.add_argument("--re-register", action="store_true", help="Lagebezug neu ermitteln, auch wenn in %%SKEL_CFG%% " "einer abgelegt ist") return parser.parse_args(argv) if __name__ == "__main__": sys.exit(main())