Files
sps_skel/lib/scl_skeleton.py
T
Michael StanglandClaude Opus 5.5 0115ef68b7 TRO-JSON v4: Slots je TRO und Staubereiche (jamAreas)
- model: Slot (separator/switch/scanner/drive + Rollen), JamArea (J<Separator>,
  Eingang eines TRO mit allen Vorgaengern), derive_slots, derive_jam_areas;
  Schema sps_skel-tro-v4, v3-Dateien werden weiter gelesen
- scl_context: ein JamEntr je eigenem Staubereich statt je Vorgaenger (wie
  UH01-UH05), Ausgaenge nennen den Staubereich des Nachfolgers
- Golden Files neu erzeugt, Tests fuer Slots/Staubereiche, Doku nachgezogen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:44:02 +02:00

566 lines
23 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 e_planning import EPLAN_SCHEMA, EplanDocument
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"{jam.title} 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"{jam.title} vor {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 context.eplan_source:
add("//")
add(f"// E-Planung: {context.eplan_source}")
add("// Separator-Sensoren und Stopperventile aus der Zuordnung sind")
add("// eingesetzt (ohne Marker); was die Zuordnung nicht sicher fand,")
add(f"// bleibt '{TODO}'. Details: <csv>_eplan.md (lib/e_planning.py).")
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 = _in_results(args.json, results)
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)
eplan = None
if args.eplan:
eplan_file = _in_results(args.eplan, results)
try:
eplan = EplanDocument.from_json(eplan_file.read_text(encoding="utf-8"))
except OSError as exc:
print(f"FEHLER: E-Planungs-Zuordnung nicht lesbar: {exc}", file=sys.stderr)
print(" Zuerst bin/e_planning.bat --file <name>.csv --eplan "
"<zeichnung>_positionsdraw.json laufen lassen.", file=sys.stderr)
return 1
except ValidationError as exc:
print(f"FEHLER: {eplan_file} passt nicht zum Schema '{EPLAN_SCHEMA}' "
f"({exc.error_count()} Abweichungen).", file=sys.stderr)
return 1
# Eine Zuordnung zu einem anderen Layout setzt falsche Namen ein -
# Separatornummern sind nur innerhalb eines CSV-Exports eindeutig.
if Path(eplan.csv_file).name != Path(document.source).name:
print(f"FEHLER: {eplan_file.name} gehoert zu '{eplan.csv_file}', die "
f"TRO-JSON zu '{document.source}'.", file=sys.stderr)
return 1
context = build_context(document, json_file.name, args.start, eplan=eplan)
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 '-'})")
if eplan is not None:
print(f"E-Planung = {eplan_file}")
print(f"Eingesetzt = {_eingesetzt(context)} Namen aus der E-Planung")
print(f"Offene Stellen = {offen} (Marker '{TODO}')")
print(f"SCL = {out_file}")
print("================================================================")
print("")
return 0
def _in_results(name: str, results: Path) -> Path:
"""Blosser Dateiname -> in SKEL_RESULTS; Pfadangaben bleiben unveraendert."""
path = Path(name)
if not path.is_absolute() and path.parent == Path("."):
return results / name
return path
def _eingesetzt(context: SclFile) -> int:
"""Wie viele Separator-Namen aus der E-Planung kamen."""
return sum(not name.todo
for tro in context.tros for s in tro.separators
for name in (s.sensor, s.stopper))
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"
" scl_skeleton.bat --json mubea_tro.json --eplan mubea_eplan.json\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(
"--eplan",
metavar="NAME",
help="Zuordnung zur E-Planung aus e_planning.py (<csv>_eplan.json in "
"%%SKEL_RESULTS%% oder ein Pfad): Separator-Sensoren und "
"Stopperventile werden eingesetzt statt als TODO markiert.",
)
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())