- OpenPoint -> model.TroCandidate (type, position, belongsTo, note); TroDocument.candidates ersetzt openPoints, Schema sps_skel-tro-v3 - Positionsbehandlung von TroBase und TroCandidate im Mixin _Positioned - tro_overrides.py: eigene ConnectionSpec/OpenPoint entfallen, stattdessen model.Connection / model.TroCandidate; einheitliche Feldnamen (tro_id/separator, hosts als Liste) - Fixtures, Beispiel-JSON und Doku auf v3 nachgezogen Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
305 lines
12 KiB
Python
305 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
tro_overrides.py - manuelle Korrekturen an der abgeleiteten TRO-Liste.
|
|
|
|
`tro_flow.py` leitet die TROs allein aus dem mechanischen Layout (CSV-Export)
|
|
ab. Manches steht dort aber nicht drin: eine Weiche, die im Export fehlt, ein
|
|
Scanner, der den Typ auf `1Sep_SSCC` hebt, oder eine Stelle, die vor Ort als
|
|
eigener TRO erkannt wurde. Solche Erkenntnisse kommen aus der Begehung bzw.
|
|
aus dem Review der erzeugten Diagramme.
|
|
|
|
Damit sie den naechsten Lauf ueberleben, stehen sie in einer INI-Datei
|
|
(`%SKEL_CFG%/tro_overrides.ini`) statt in den erzeugten Dateien.
|
|
|
|
Seit 2026-10-05 nur noch von `material_flow.py --connect` gelesen (Abschnitt
|
|
`[<Anlage>.manual]` der connect.ini). `tro_flow.py` und `tro_annotate.py`
|
|
wenden keine Korrekturen mehr an: die Abschnitte hingen an laufenden
|
|
TRO-Nummern und veralteten mit jedem Export - Fehler der Ableitung werden
|
|
seitdem im Code behoben (lib/tro_flow.py), nicht in der INI.
|
|
|
|
Aufbau der INI (Sektionsnamen mit dem CSV-Dateinamen als Praefix, damit
|
|
mehrere Anlagen in einer Datei stehen koennen):
|
|
|
|
[<csv>.tro.<TRO-ID>] Korrektur an einem abgeleiteten TRO
|
|
type = 1Sep1Swi neuer Katalogtyp
|
|
reason = ... Begruendung (kommt in die Doku)
|
|
confidence = bestaetigt Vertrauensangabe (Default: bestaetigt)
|
|
|
|
[<csv>.openpoint.<Name>] offener Punkt - noch KEIN TRO (model.TroCandidate)
|
|
x = 529.95 Anlagenkoordinate in mm
|
|
y = -3895.27
|
|
z = 1468
|
|
expect = 1Sep1Swi hier erwarteter TRO-Typ (-> type_name)
|
|
belongs_to = TRO10 TRO, der das Bauteil heute enthaelt
|
|
note = ... Erlaeuterung
|
|
|
|
Offene Punkte aendern die TRO-Struktur ausdruecklich **nicht**.
|
|
|
|
Die Felder heissen wie in lib/model.py (`type_name`, `separators`, `hosts`,
|
|
`reason`, `confidence`); Verbindungen und offene Punkte sind direkt
|
|
`model.Connection` bzw. `model.TroCandidate`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import configparser
|
|
import os
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from model import Connection, TroCandidate
|
|
from tro_catalog import get_tro
|
|
|
|
OVERRIDE_FILE = "tro_overrides.ini"
|
|
|
|
# Vertrauensangabe fuer manuell gesetzte Typen. Bewusst ein eigener Wert - so
|
|
# ist in der Doku sichtbar, dass der Typ nicht aus dem Layout stammt.
|
|
DEFAULT_CONFIDENCE = "bestaetigt"
|
|
|
|
|
|
@dataclass
|
|
class TroOverride:
|
|
"""Korrektur an einem abgeleiteten TRO.
|
|
|
|
Adressiert entweder ueber die TRO-ID (`TRO03`) oder - stabiler - ueber die
|
|
Nummer eines seiner Separatoren (`SEP0011`). Die TRO-IDs sind laufende
|
|
Nummern eines Laufs und verschieben sich, sobald TROs dazukommen oder
|
|
wegfallen; die Separator-Nummer aus dem CSV-Export tut das nicht.
|
|
"""
|
|
|
|
# genau eins von beiden ist gesetzt
|
|
tro_id: str = ""
|
|
separator: str = ""
|
|
type_name: str = ""
|
|
reason: str = ""
|
|
confidence: str = ""
|
|
|
|
def matches(self, tro) -> bool:
|
|
if self.separator:
|
|
return self.separator in tro.separators
|
|
return self.tro_id == tro.tro_id
|
|
|
|
|
|
@dataclass
|
|
class SplitSpec:
|
|
"""Ein neuer TRO, herausgeloest aus einem bereits abgeleiteten.
|
|
|
|
Fuer Uebergabestellen, die die Ableitung nicht als eigenstaendig erkennt,
|
|
weil die "Zuordnung" des Separators auf ein Transportobjekt zeigt (Strecke,
|
|
Gefaellestrecke) statt auf die Kreisel-Bahn, an der er tatsaechlich
|
|
schaltet. Die genannten Separatoren/Scanner werden ihrem bisherigen TRO
|
|
entnommen und bilden einen neuen.
|
|
"""
|
|
|
|
name: str
|
|
type_name: str
|
|
separators: list[str] = field(default_factory=list)
|
|
scanners: list[str] = field(default_factory=list)
|
|
hosts: list[str] = field(default_factory=list)
|
|
label: str = ""
|
|
reason: str = ""
|
|
confidence: str = DEFAULT_CONFIDENCE
|
|
|
|
|
|
@dataclass
|
|
class MergeSpec:
|
|
"""Mehrere abgeleitete TROs zu einem Steuerobjekt zusammenfassen.
|
|
|
|
Fuer Stellen, die mechanisch aus mehreren Bauteilgruppen bestehen,
|
|
steuerungstechnisch aber eine einzige Uebergabestelle sind - etwa zwei
|
|
Sperren beiderseits eines Kreisel-Beruehrpunkts. Adressiert wird ueber die
|
|
Separatoren, weil deren Nummern ueber Exporte hinweg stabiler sind als die
|
|
laufenden TRO-Nummern.
|
|
"""
|
|
|
|
name: str
|
|
type_name: str
|
|
separators: list[str] = field(default_factory=list)
|
|
hosts: list[str] = field(default_factory=list)
|
|
label: str = ""
|
|
reason: str = ""
|
|
confidence: str = DEFAULT_CONFIDENCE
|
|
|
|
|
|
@dataclass
|
|
class Overrides:
|
|
"""Alle Korrekturen zu einer CSV-Datei."""
|
|
|
|
source: Path | None = None
|
|
tros: dict[str, TroOverride] = field(default_factory=dict)
|
|
splits: list[SplitSpec] = field(default_factory=list)
|
|
merges: list[MergeSpec] = field(default_factory=list)
|
|
# Gesetzte Topologie: ersetzt die abgeleiteten Kanten vollstaendig
|
|
connections: list[Connection] = field(default_factory=list)
|
|
candidates: list[TroCandidate] = field(default_factory=list)
|
|
# Protokoll der tatsaechlich angewandten Korrekturen (fuer die Doku)
|
|
applied: list[str] = field(default_factory=list)
|
|
warnings: list[str] = field(default_factory=list)
|
|
|
|
def __bool__(self) -> bool:
|
|
return bool(self.tros or self.splits or self.merges or self.connections
|
|
or self.candidates)
|
|
|
|
|
|
def override_path(cfg_dir: Path | None = None) -> Path:
|
|
"""Pfad zur Override-Datei: %SKEL_CFG%, sonst <projekt>/cfg."""
|
|
if cfg_dir is not None:
|
|
return cfg_dir / OVERRIDE_FILE
|
|
env = os.environ.get("SKEL_CFG")
|
|
if env:
|
|
return Path(env) / OVERRIDE_FILE
|
|
return Path(__file__).resolve().parent.parent / "cfg" / OVERRIDE_FILE
|
|
|
|
|
|
def _as_float(raw: str, where: str, warnings: list[str]) -> float | None:
|
|
try:
|
|
return float(str(raw).strip())
|
|
except (TypeError, ValueError):
|
|
warnings.append(f"{where}: '{raw}' ist keine Zahl - Eintrag ignoriert")
|
|
return None
|
|
|
|
|
|
def _as_list(raw: str) -> list[str]:
|
|
return [s.strip() for s in raw.split(",") if s.strip()]
|
|
|
|
|
|
def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
|
"""Korrekturen zu `csv_name` laden (leeres Ergebnis, wenn es keine gibt)."""
|
|
path = override_path(cfg_dir)
|
|
result = Overrides()
|
|
if not path.is_file():
|
|
return result
|
|
result.source = path
|
|
|
|
parser = configparser.ConfigParser(inline_comment_prefixes=(";",))
|
|
# Schluessel sind Kleinschreibung, Sektionsnamen bleiben wie geschrieben.
|
|
parser.read(path, encoding="utf-8")
|
|
|
|
prefix = csv_name.lower()
|
|
for section in parser.sections():
|
|
# [<csv>.connections] - gesetzte Topologie, eigene Form ohne <name>
|
|
if section.lower() == f"{prefix}.connections":
|
|
for key, raw in parser[section].items():
|
|
pfeil, _, rest = str(raw).partition("|")
|
|
src, sep, dst = (t.strip() for t in pfeil.partition("->"))
|
|
if not sep or not src or not dst:
|
|
result.warnings.append(
|
|
f"[{section}] {key}: '{raw}' ist keine Verbindung "
|
|
f"'<von> -> <nach>'"
|
|
)
|
|
continue
|
|
result.connections.append(
|
|
Connection(src=src, dst=dst, kind=rest.strip())
|
|
)
|
|
continue
|
|
|
|
parts = section.split(".")
|
|
# <csv>.<art>.<name> - der CSV-Name kann selbst einen Punkt enthalten
|
|
if len(parts) < 3:
|
|
continue
|
|
kind, name = parts[-2].lower(), parts[-1]
|
|
owner = ".".join(parts[:-2]).lower()
|
|
if owner != prefix:
|
|
continue
|
|
values = parser[section]
|
|
|
|
if kind == "tro":
|
|
type_name = values.get("type", "").strip()
|
|
if type_name and get_tro(type_name) is None:
|
|
result.warnings.append(
|
|
f"[{section}]: Typ '{type_name}' steht nicht im TRO-Katalog "
|
|
f"- Korrektur ignoriert"
|
|
)
|
|
continue
|
|
key = name.upper()
|
|
by_sep = key.startswith("SEP")
|
|
result.tros[key] = TroOverride(
|
|
tro_id="" if by_sep else key,
|
|
separator=key[3:] if by_sep else "",
|
|
type_name=type_name,
|
|
reason=values.get("reason", "").strip(),
|
|
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
|
|
)
|
|
elif kind == "split":
|
|
type_name = values.get("type", "").strip()
|
|
if get_tro(type_name) is None:
|
|
result.warnings.append(
|
|
f"[{section}]: Typ '{type_name}' steht nicht im TRO-Katalog "
|
|
f"- neuer TRO nicht angelegt"
|
|
)
|
|
continue
|
|
seps = _as_list(values.get("separators", ""))
|
|
if not seps:
|
|
result.warnings.append(
|
|
f"[{section}]: 'separators' fehlt - ohne Separator kein TRO"
|
|
)
|
|
continue
|
|
result.splits.append(
|
|
SplitSpec(
|
|
name=name,
|
|
type_name=type_name,
|
|
separators=seps,
|
|
scanners=_as_list(values.get("scanners", "")),
|
|
hosts=_as_list(values.get("hosts", values.get("host", ""))),
|
|
label=values.get("label", "").strip(),
|
|
reason=values.get("reason", "").strip(),
|
|
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
|
|
)
|
|
)
|
|
elif kind == "merge":
|
|
type_name = values.get("type", "").strip()
|
|
if get_tro(type_name) is None:
|
|
result.warnings.append(
|
|
f"[{section}]: Typ '{type_name}' steht nicht im TRO-Katalog "
|
|
f"- Zusammenfassung nicht durchgefuehrt"
|
|
)
|
|
continue
|
|
seps = _as_list(values.get("separators", ""))
|
|
if len(seps) < 2:
|
|
result.warnings.append(
|
|
f"[{section}]: 'separators' braucht mindestens zwei Nummern"
|
|
)
|
|
continue
|
|
result.merges.append(
|
|
MergeSpec(
|
|
name=name,
|
|
type_name=type_name,
|
|
separators=seps,
|
|
hosts=_as_list(values.get("hosts", values.get("host", ""))),
|
|
label=values.get("label", "").strip(),
|
|
reason=values.get("reason", "").strip(),
|
|
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
|
|
)
|
|
)
|
|
elif kind == "openpoint":
|
|
x = _as_float(values.get("x", ""), f"[{section}] x", result.warnings)
|
|
y = _as_float(values.get("y", ""), f"[{section}] y", result.warnings)
|
|
if x is None or y is None:
|
|
continue
|
|
z_raw = values.get("z", "").strip()
|
|
z = _as_float(z_raw, f"[{section}] z", result.warnings) if z_raw else None
|
|
type_name = values.get("expect", "").strip()
|
|
if type_name and get_tro(type_name) is None:
|
|
result.warnings.append(
|
|
f"[{section}]: erwarteter Typ '{type_name}' steht nicht im "
|
|
f"TRO-Katalog"
|
|
)
|
|
result.candidates.append(
|
|
TroCandidate(
|
|
name=name,
|
|
type_name=type_name,
|
|
position=(x, y, z if z is not None else 0.0),
|
|
belongs_to=values.get("belongs_to", "").strip(),
|
|
note=values.get("note", "").strip(),
|
|
)
|
|
)
|
|
else:
|
|
result.warnings.append(
|
|
f"[{section}]: unbekannte Art '{kind}' - erwartet 'tro', 'split', "
|
|
f"'merge' oder 'openpoint'"
|
|
)
|
|
|
|
result.candidates.sort(key=lambda p: p.name)
|
|
return result
|