Files
sps_skel/lib/scl_skeleton.py
T
Samer AyadiandClaude Opus 5.5 4895ebf5ed SCL aus parametrisierten Templates: Golden-Files, Render-Kontext, Pilot 1Sep1Swi
Schritte 1-3 aus doc/SCL_Templates.md (neu: Konzept, Wertklassen je Zeile,
Namensregeln, Tests, Migrationsplan, Vertrag fuer den CSV->SCL-Workflow).

- Golden-Files (tests/test_scl_skeleton_golden.py, tests/fixtures/scl/):
  komplette FB_Main-Ausgabe von mubea und HundM05 byte-genau festgeschrieben,
  Zeitstempel maskiert; gewollte Aenderung per SKEL_UPDATE_GOLDEN=1
- lib/scl_context.py (neu): alles, was scl_skeleton aus der TRO-JSON ableitet
  (Reihenfolge, Separator-/Weichen-/Scanner-Bloecke, Staubereiche, Ziellisten,
  Namen) als pydantic-Kontext SclFile/SclTro; Namen als SclName (Wert +
  todo-Merker) als Andockstelle fuer die Namensregeln (Schritt 4).
  scl_skeleton schreibt nur noch Text; Ausgabe byte-identisch (Golden-Files,
  zusaetzlich dematic/showroom mit und ohne --start alt/neu verglichen)
- lib/scl_templates/ (neu, Jinja2): _macros.scl.j2 + Pilot
  FB_ILS_MTRO_1Sep1Swi.scl.j2 nach der realen Aufrufstelle TRO107
  (PriorityManagerDir2, stInSenJam<n>, xInCarouselRun1, xInRelease TRUE,
  Separatorzeiten 100/50 ms). Ortsgebundene Zeilen als Vorschlag umgesetzt,
  jede offene Stelle mit TODO(E-Planung) - Bestaetigung durch SPS steht aus
- scl_skeleton: --templates [DIR] (opt-in, ohne Schalter Ausgabe unveraendert),
  --strict (Exit 1 ohne SCL, wenn ein FB-Typ kein Template hat); Kopf und
  Zusammenfassung nennen Template-/generische Typen; Templatefehler
  (StrictUndefined) -> Exit 1 ohne SCL
- tests/test_scl_templates.py: Rundlauf Template mit Referenzwerten ==
  Referenz-REGION (normalisiert); jede Platzhalterzeile traegt den TODO-Marker
- requirements.txt: jinja2 >= 3.1 (bestehende .venv: pip install -r requirements.txt)
- Doku: SCL_Templates.md, Python_Scripts.md §5b, Commandlines.md §2.5, CLAUDE.md

Befunde: xInCarouselRun1 laesst sich nicht aus der TRO-JSON ableiten (Kanten-
art "umlauf" kommt praktisch nicht vor) -> immer als TODO; nTo1Destinations
ist in der Referenz 3 bei 2 Abgaengen (zaehlt evtl. Ziele) -> "pruefen".
Mit --templates nur die 1Sep1Swi-REGIONs (+ Kopf) anders als generisch;
32 Tests gruen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:06:18 +02:00

514 lines
21 KiB
Python

# -*- coding: utf-8 -*-
# Absichtlich ohne Shebang: der Windows-py-Launcher wuerde daraus "python3"
# ableiten und scheitert, wenn keine PythonCore-Installation registriert ist.
"""
scl_skeleton.py - FB_Main-Geruest als SCL aus der TRO-Liste erzeugen.
Letzter Schritt der Kette:
material_flow.py Objekte -> Materialfluss
tro_flow.py Fluss -> TROs -> <name>_tro.json (--json)
scl_skeleton.py JSON -> FB_Main.scl <- hier
Die TRO-JSON wird ueber das gemeinsame Modell lib/model.py (TroDocument)
eingelesen und dabei geprueft: ein falscher oder umbenannter Schluessel bricht
mit einer Fehlermeldung ab, statt still einen Standardwert zu nehmen. Bauteile
(items) und FB-Typ kommen aus tro_catalog, ueber den TRO-Typ.
Erzeugt wird ein **Geruest**, kein fertiges Programm: je TRO eine REGION mit
Instanzaufruf und allen Parameterzeilen in der richtigen Reihenfolge, so wie es
die realen Aufrufstellen in doc/TRO_Katalog/scl_templates/*.scl zeigen.
Was aus dem Layout kommt, steht drin. Was aus der E-Planung kommen muss, steht
als Platzhalter mit `TODO(E-Planung)` da - bewusst als offensichtliche Luecke
und nicht als erfundener Wert, denn eine plausibel aussehende falsche
Sensoradresse ist schlimmer als eine leere. Alle Platzhalter sind ueber genau
diesen Marker auffindbar.
Ableitbar und daher gefuellt:
- Reihenfolge der REGIONs (Materialfluss), Instanzliste, FB-Typ je TRO
- Anzahl der Separator-/Weichen-/Scanner-Parameterbloecke je TRO
- nTo1Destinations einer Weiche = Anzahl ihrer Abgaenge
- die Zielliste je Weichenabgang (Erreichbarkeit im TRO-Graphen)
- Querverweis auf die Bauteile im Layout (Separatornummern, Koordinate)
Aufruf ueber bin/scl_skeleton.bat bzw. bin/scl_skeleton.sh.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from jinja2 import (
Environment,
FileSystemLoader,
StrictUndefined,
TemplateError,
TemplateNotFound,
)
from pydantic import ValidationError
from material_flow import env_dir
from model import SCHEMA, TroDocument
from scl_context import TODO, SclFile, SclName, SclTro, build_context
# Parametrisierte Aufrufstellen je FB (doc/SCL_Templates.md); Dateiname = fbType.
TEMPLATE_DIR = Path(__file__).resolve().parent / "scl_templates"
TEMPLATE_SUFFIX = ".scl.j2"
class SclTemplates:
"""Die FB-Templates eines Ordners; ohne Template -> generischer Aufbau."""
def __init__(self, folder: Path) -> None:
self.folder = folder
self.env = Environment(
loader=FileSystemLoader(str(folder)),
# Tippfehler im Template -> Fehler statt leerer SCL-Stelle
undefined=StrictUndefined,
trim_blocks=True,
lstrip_blocks=True,
keep_trailing_newline=True,
autoescape=False,
)
self.env.globals["TODO"] = TODO
def has(self, fb: str) -> bool:
return (self.folder / f"{fb}{TEMPLATE_SUFFIX}").is_file()
def render(self, tro: SclTro) -> str:
"""REGION eines TRO aus seinem Template (ohne abschliessenden Umbruch)."""
try:
template = self.env.get_template(f"{tro.fb}{TEMPLATE_SUFFIX}")
except TemplateNotFound as exc:
raise TemplateError(f"Template fuer {tro.fb} fehlt: {exc}") from exc
# Leerzeilen hier zusammenfassen statt im Template mit Whitespace-
# Steuerung ({%- ... -%}) - die macht Templates schwer lesbar und
# bricht leicht die Einrueckung.
zeilen: list[str] = []
for zeile in template.render(t=tro).split("\n"):
zeile = zeile.rstrip()
if zeile or (zeilen and zeilen[-1]):
zeilen.append(zeile)
return "\n".join(zeilen).strip("\n")
# Zeitwerte aus den Referenz-Aufrufstellen. Sie sind plausible Startwerte, KEINE
# fuer diese Anlage ermittelten - deshalb ueberall mit Pruefhinweis.
DEFAULT_SEP_TIMES = (
("tDelayToNextItem", '"TimeForSepWaiting_Short"', None),
("tTrailingTime", "t#600ms", "pruefen"),
("tHandlingTime", "t#200ms", "pruefen"),
("tJam", '"TimeForSepJam"', None),
)
def load_document(path: Path) -> TroDocument:
"""TRO-JSON einlesen und gegen das Modell pruefen."""
return TroDocument.from_json(path.read_text(encoding="utf-8"))
def _todo(name: SclName, abstand: str) -> str:
"""TODO-Kommentar hinter einer Zeile, wenn der Name noch offen ist."""
return f"{abstand}// {TODO}" if name.todo else ""
def emit_separators(add, tro: SclTro) -> None:
for s in tro.separators:
pfad = f"#{tro.id}.stInSeparator{s.n}"
add(f" // Separator {s.n} = Bauteil {s.layout} aus dem Layout")
add(f' {pfad}.Settings.nSeparatorNo := "{s.no.value}";{_todo(s.no, " ")}')
for name, wert, hinweis in DEFAULT_SEP_TIMES:
kommentar = f" // {TODO}: {hinweis}" if hinweis else ""
add(f" {pfad}.Settings.{name} := {wert};{kommentar}")
add(f" {pfad}.Settings.xPluggedJam := TRUE;")
add(f" {pfad}.Settings.xPluggedPart := FALSE;")
add(f" {pfad}.Settings.xPluggedScanner := "
f"{'TRUE' if tro.has_scanner else 'FALSE'};")
add(f' {pfad}.stSenInSep := "DB_Inputs".Sensors["{s.sensor.value}"];'
f'{_todo(s.sensor, " ")}')
add(f" {pfad}.Settings.tSenFree := T#1500ms; // {TODO}: pruefen")
add(f" {pfad}.Settings.tSenWait := T#4000ms; // {TODO}: pruefen")
add("")
def emit_switches(add, tro: SclTro) -> None:
for w in tro.switches:
pfad = f"#{tro.id}.stInSwitch{w.n}"
add(f' {pfad}.Settings.nSwitchNo := "{w.no.value}";{_todo(w.no, " ")}')
add(f" {pfad}.Settings.xPluggedExSen3 := FALSE;")
add(f" {pfad}.Settings.xPluggedExSen4 := FALSE;")
if w.to1_destinations is not None:
add(f" {pfad}.Settings.nTo1Destinations := {w.to1_destinations};"
f" // Abgaenge laut Topologie")
else:
# Bei mehreren Weichen ist es eine Planungsentscheidung, welche
# Weiche welche Abgaenge bedient - die Summe ist bekannt, die
# Aufteilung nicht. Deshalb hier kein geratener Wert.
add(f" {pfad}.Settings.nTo1Destinations := _;"
f" // {TODO}: dieser TRO hat {len(tro.successors)} "
f"Abgaenge auf {len(tro.switches)} Weichen - Aufteilung festlegen")
add("")
if tro.switch_exits:
add(" // Zielliste je Abgang (aus dem TRO-Graphen berechnet) -")
add(" // Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:")
for abgang in tro.switch_exits:
ziele = ", ".join(abgang.targets) if abgang.targets else "-"
add(f" // ueber {abgang.via}: {ziele}")
add("")
def emit_vario(add, tro: SclTro) -> None:
pfad = f"#{tro.id}.stInVario"
add(f" {pfad}.Settings.xContinousEmptying := FALSE;")
add(f" {pfad}.Settings.tWaitForJog := T#300ms; // {TODO}: pruefen")
add(f" {pfad}.Settings.tConvEmpty := T#45000ms; // {TODO}: pruefen")
add(f" {pfad}.Settings.nCountCarrier := 1; // {TODO}: pruefen")
add(f" {pfad}.Settings.tWaitForStop := t#3s;")
add(f" {pfad}.Settings.tCarrierInPosDelay := t#1200ms;")
add(f" {pfad}.Settings.tCarrierInPosEdgeDelay := t#100ms;")
add(f" {pfad}.Settings.xJamSenDelayActive := TRUE;")
add(f" {pfad}.Settings.tJamSenDelay := T#2s;")
# Der Kommentar steht bei allen Sensoren in derselben Spalte
# (Parametername + Abstand = 16 Zeichen).
for name, sensor in tro.vario_sensors.items():
abstand = " " * (16 - len(name))
add(f' {pfad}.{name} := "DB_Inputs".Sensors["{sensor.value}"];'
f"{_todo(sensor, abstand)}")
add("")
def emit_barcode(add, tro: SclTro) -> None:
for scanner in tro.scanners:
pfad = f"#{tro.id}.stInBarcodeReader{scanner.suffix}"
add(f" {pfad}.Settings.xActivateCheckASCI := TRUE;")
add(f" {pfad}.Settings.tTimeout := T#3000ms;")
add(f' // {TODO}: Cognex-Interface verdrahten '
f'(#arCognexInterface["cScanner____"])')
add("")
def emit_call(add, tro: SclTro) -> None:
"""Aufrufstelle schreiben.
Die Zeilen werden als (Code, Kommentar) gefuehrt und das Komma erst beim
Zusammensetzen gesetzt - sonst landet es hinter dem "//" und ist Teil des
Kommentars, womit die Parameterliste nicht mehr uebersetzt.
"""
def offen(name: SclName, text: str = "") -> str:
if not name.todo:
return text
return f"{TODO} - {text}" if text else TODO
zeilen: list[tuple[str, str]] = [
(f'nInMainTroNo := "{tro.main_tro.value}"', offen(tro.main_tro)),
(f"xInSftyOk := {tro.safety.value}", offen(tro.safety)),
("xInAllRdyToStart := TRUE", ""),
(f"xInRelease := {tro.release}", ""),
]
if tro.has_vario:
zeilen.append((f"xInMotorProtection := {tro.motor_protection.value}",
offen(tro.motor_protection)))
zeilen.append(("xInAutomatic := #t_automatik", ""))
# Der generische Aufbau hat den Schaltschrank nie als offen markiert;
# das bleibt so, damit seine Ausgabe stabil ist (Golden-Files). Die
# Templates markieren ihn.
zeilen.append((f"stInOutMachineState := {tro.machine_state.value}", ""))
zeilen.append(("stInOutCognexInterface := #stCognexInterfaceDummy", ""))
for jam in tro.jam_entries:
zeilen.append((f"arInOutJamEntr{jam.n} := {jam.db.value}.arCarrier",
offen(jam.db, f"Staubereich von {jam.neighbour}")))
zeilen.append((f"stInOutJamEntr{jam.n} := {jam.db.value}.stData", ""))
for jam in tro.jam_exits:
zeilen.append((f"arInOutJamExit{jam.n} := {jam.db.value}.arCarrier",
offen(jam.db, f"Staubereich nach {jam.neighbour}")))
zeilen.append((f"stInOutJamExit{jam.n} := {jam.db.value}.stData", ""))
for s in tro.separators:
zeilen.append((f'xOutStopper{s.n} => "{s.stopper.value}"', offen(s.stopper)))
if tro.has_vario:
zeilen.append((f'xOutVarioMotor => "{tro.vario_motor.value}"',
offen(tro.vario_motor)))
for w in tro.switches:
zeilen.append((f'xOutSw{w.n}ExTo3 => "{w.valve.value}"', offen(w.valve)))
zeilen.append((f'stInOutHMI := "DB_Interface_HMI".stTRO.{tro.id}', ""))
zeilen.append((f'nOutStateLast => "DB_GLOB_TroState".arTroState["{tro.main_tro.value}"]',
offen(tro.main_tro)))
kopf = f" #{tro.id}("
einzug = " " * len(kopf)
for index, (code, kommentar) in enumerate(zeilen):
trenner = ");" if index == len(zeilen) - 1 else ","
prefix = kopf if index == 0 else einzug
text = f"{prefix}{code}{trenner}"
add(f"{text} // {kommentar}" if kommentar else text)
def emit_direction(add, tro: SclTro) -> None:
"""Weichenlogik - je Weiche ein FC_Direction-Aufruf."""
add("")
add(" REGION Direction")
for w in tro.switches:
add(f' "FC_Direction"(nInSwitchNo := "{w.no.value}",{_todo(w.no, " ")}')
add(f" nOutDirection => {w.direction_target},")
add(f" InOutCarrier := {w.jam_db.value}.arCarrier[1]);")
if w.n < len(tro.switches):
add("")
add(" END_REGION")
def emit_region(add, tro: SclTro) -> None:
"""Eine REGION je TRO (heutiger, generischer Aufbau aus den Bauteilen)."""
add("")
add(f"REGION {tro.region_title}")
if tro.position:
add(f" // Anlagenkoordinate x/y: {tro.position[0]:.0f} / {tro.position[1]:.0f} mm")
add(f" // Vorgaenger: {', '.join(tro.predecessors) or '-'}")
add(f" // Nachfolger: {', '.join(tro.successors) or '-'}")
if tro.confidence:
add(f" // Einstufung: {tro.confidence}")
add("")
emit_separators(add, tro)
if tro.switches:
emit_switches(add, tro)
if tro.has_vario:
emit_vario(add, tro)
if tro.scanners:
emit_barcode(add, tro)
if tro.storage_lines:
add(f" // {TODO}: Linienspeicher mit {tro.storage_lines} Linien - "
f"Parametrierung je Linie")
add("")
add(" #%s.stInPriorityManager.xReleaseOk := TRUE;" % tro.id)
add("")
emit_call(add, tro)
if tro.switches:
emit_direction(add, tro)
add("END_REGION")
def template_usage(context: SclFile, templates: SclTemplates | None
) -> tuple[list[str], list[str]]:
"""FB-Typen mit bzw. ohne Template (je sortiert, ohne Doppelte)."""
fbs = sorted({t.fb for t in context.tros})
if templates is None:
return [], fbs
return ([fb for fb in fbs if templates.has(fb)],
[fb for fb in fbs if not templates.has(fb)])
def render_context(context: SclFile, templates: SclTemplates | None = None) -> str:
"""SCL-Text aus dem fertigen Kontext.
Mit ``templates`` wird jeder TRO, fuer dessen FB ein Template existiert, aus
dem Template erzeugt, alle anderen generisch. Ohne ``templates`` ist die
Ausgabe die bisherige (Golden-Files in tests/fixtures/scl/).
"""
out: list[str] = []
add = out.append
add("// ============================================================")
add("// FB_Main - GERUEST, erzeugt von lib/scl_skeleton.py")
add(f"// Quelle : {context.source}")
add(f"// Erzeugt: {context.created}")
add("//")
add("// Das ist KEIN lauffaehiges Programm, sondern ein Geruest zum")
add("// Ausfuellen. Struktur, Reihenfolge und Bauteilzahl stammen aus dem")
add("// Anlagenlayout und der vor Ort aufgenommenen Topologie.")
add("//")
add(f"// Jede Stelle, die aus der E-Planung kommen muss, ist mit")
add(f"// '{TODO}' markiert - danach suchen, dann ist nichts vergessen.")
add("// Offen sind durchgaengig: Sensor-/Aktoradressen (cInBG/MB/MA),")
add("// Separator- und Weichennummern, MainTro-Nummern, die Staubereiche")
add("// (DB_JamArea...) und die Zeiten, soweit als 'pruefen' vermerkt.")
add("//")
add("// Reihenfolge der REGIONs = Materialfluss.")
add(f"// Anfang bei {context.tros[0].id}: {context.start_reason}.")
if templates is not None:
mit, ohne = template_usage(context, templates)
add("//")
add(f"// Aus Template : {', '.join(mit) or '-'}")
add(f"// Generisch : {', '.join(ohne) or '-'} (kein Template - von Hand pruefen)")
add("// ============================================================")
add("")
add('FUNCTION_BLOCK "FB_Main"')
add("{ S7_Optimized_Access := 'TRUE' }")
add("VERSION : 0.1")
add(" VAR ")
for tro in context.tros:
kommentar = f" // {tro.type}"
if tro.layout_separators:
kommentar += f", Sep {', '.join(tro.layout_separators)}"
add(f' {tro.id} : "{tro.fb}";{kommentar}')
add(" END_VAR")
add("")
add("BEGIN")
for tro in context.tros:
if templates is not None and templates.has(tro.fb):
add("")
out.extend(templates.render(tro).split("\n"))
else:
emit_region(add, tro)
add("")
add("END_FUNCTION_BLOCK")
add("")
return "\n".join(out)
def render(document: TroDocument, quelle: str, start_id: str = "",
templates: SclTemplates | None = None) -> str:
"""TRO-JSON -> SCL-Text (Kontext bauen, dann ausgeben)."""
return render_context(build_context(document, quelle, start_id), templates)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
results = env_dir("SKEL_RESULTS", "results")
json_file = Path(args.json)
if not json_file.is_absolute() and json_file.parent == Path("."):
json_file = results / args.json
if not json_file.is_file():
print(f"FEHLER: JSON nicht gefunden: {json_file}", file=sys.stderr)
print(" Zuerst bin/tro_flow.bat --file <name>.csv --json laufen lassen.",
file=sys.stderr)
return 1
try:
document = load_document(json_file)
except OSError as exc:
print(f"FEHLER: {json_file} nicht lesbar: {exc}", file=sys.stderr)
return 1
except ValidationError as exc:
schema = _schema_of(json_file)
if schema and schema != SCHEMA:
# Anderes Format (z. B. die alte tro_extract-JSON) - die einzelnen
# Feldfehler helfen dann nicht weiter.
print(f"FEHLER: {json_file} hat das Schema '{schema}', erwartet wird "
f"'{SCHEMA}' ({exc.error_count()} Abweichungen).", file=sys.stderr)
print(" Mit bin/tro_flow.bat --file <name>.csv --json neu erzeugen.",
file=sys.stderr)
return 1
print(f"FEHLER: {json_file} passt nicht zum TRO-Modell (lib/model.py):",
file=sys.stderr)
print(exc, file=sys.stderr)
return 1
if not document.tros:
print(f"FEHLER: {json_file} enthaelt keine TROs.", file=sys.stderr)
return 1
templates = None
if args.templates is not None:
ordner = Path(args.templates)
if not ordner.is_dir():
print(f"FEHLER: Template-Ordner nicht gefunden: {ordner}", file=sys.stderr)
return 1
templates = SclTemplates(ordner)
context = build_context(document, json_file.name, args.start)
mit, ohne = template_usage(context, templates)
if args.strict and ohne:
print(f"FEHLER: kein Template fuer {', '.join(ohne)} (--strict) - "
f"keine SCL geschrieben.", file=sys.stderr)
return 1
try:
text = render_context(context, templates)
except TemplateError as exc:
# StrictUndefined, Syntaxfehler, fehlendes Makro ... - lieber keine SCL
# als eine mit stillen Luecken.
print(f"FEHLER im Template: {exc}", file=sys.stderr)
return 1
out_file = results / (args.out or f"{json_file.stem.replace('_tro', '')}_FB_Main.scl")
out_file.write_text(text, encoding="utf-8")
offen = text.count(TODO)
print("")
print("================================================================")
print("FB_MAIN-GERUEST")
print("================================================================")
print(f"Quelle = {json_file}")
print(f"TROs = {len(context.tros)}")
print(f"Reihenfolge = {' -> '.join(t.id for t in context.tros)}")
print(f"Anfang = {context.tros[0].id} ({context.start_reason})")
if templates is not None:
print(f"Templates = {len(mit)} von {len(mit) + len(ohne)} Typen "
f"(generisch: {', '.join(ohne) or '-'})")
print(f"Offene Stellen = {offen} (Marker '{TODO}')")
print(f"SCL = {out_file}")
print("================================================================")
print("")
return 0
def _schema_of(path: Path) -> str:
"""Schema-Kennung einer JSON-Datei, nur fuer die Fehlermeldung ("" wenn unlesbar)."""
try:
return str(json.loads(path.read_text(encoding="utf-8")).get("schema", ""))
except (OSError, ValueError, AttributeError):
return ""
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="scl_skeleton",
description="Erzeugt aus der TRO-JSON (tro_flow --json) ein FB_Main-Geruest "
"als SCL zum Import in TIA Portal.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="Beispiele:\n"
" scl_skeleton.bat\n"
" scl_skeleton.bat --json export_tro.json --out FB_Main.scl\n"
" scl_skeleton.bat --json mubea_tro.json --templates\n"
"\n"
"Exit-Codes:\n"
" 0 ok (auch wenn TROs ohne Template generisch erzeugt wurden)\n"
" 1 Eingabe-, Aufruf- oder Templatefehler; mit --strict auch\n"
" ein FB-Typ ohne Template\n",
)
parser.add_argument(
"--json",
default="export_tro.json",
metavar="NAME",
help="TRO-JSON aus tro_flow.py --json in %%SKEL_RESULTS%% "
"(oder ein vollstaendiger Pfad). Standard: %(default)s",
)
parser.add_argument(
"--out",
metavar="NAME",
help="Name der SCL-Datei in %%SKEL_RESULTS%%. Standard: <Quelle>_FB_Main.scl",
)
parser.add_argument(
"--start",
default="",
metavar="TRO",
help="TRO, bei dem die Reihenfolge beginnt. Nur noetig, solange der "
"Fluss ein geschlossener Kreis ohne Be-/Entladestation ist.",
)
parser.add_argument(
"--templates",
nargs="?",
const=str(TEMPLATE_DIR),
default=None,
metavar="DIR",
help="REGIONs aus den FB-Templates erzeugen (doc/SCL_Templates.md). "
"Ohne DIR: lib/scl_templates. FB-Typen ohne Template bleiben "
"generisch. Ohne diesen Schalter: bisherige Ausgabe.",
)
parser.add_argument(
"--strict",
action="store_true",
help="Nur mit --templates: abbrechen (Exit 1), wenn ein FB-Typ der "
"Anlage kein Template hat.",
)
return parser.parse_args(argv)
if __name__ == "__main__":
sys.exit(main())