diff --git a/CLAUDE.md b/CLAUDE.md index 484eb32..0ff79aa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -152,7 +152,7 @@ extraction, not code to execute or modify. Since 2026-10-09 the main path is **CSV → `tro_flow.py --json` → TRO-JSON → `scl_skeleton.py` → SCL**. Layer order (who may import whom): `tro_catalog.py` (TRO *types*) ← `lib/model.py` (TRO *instances* and the TRO-JSON: `TroBase` → `DerivedTro`, `Connection`, -`OpenPoint`, `Analysis`, `TroDocument`) ← derivation (`material_flow.py`, `tro_flow.py`) ← +`TroCandidate`, `Analysis`, `TroDocument`) ← derivation (`material_flow.py`, `tro_flow.py`) ← outputs (`scl_skeleton.py`, `tro_annotate.py`, reports). `items`/`fbType` are always read from the catalog via the TRO type, never stored. See `doc/Python_Scripts.md` §6b. Inside the SCL output, `lib/scl_context.py` (TRO-JSON → render context, all derivations) @@ -165,7 +165,7 @@ and status: `doc/SCL_Templates.md`. Golden files in `tests/fixtures/scl/` pin th byte — rewrite them (`SKEL_UPDATE_GOLDEN=1`) only for a reviewed, intended change. 1. **`lib/tro_flow.py --json`** (implemented) — writes `_tro.json` (schema - `sps_skel-tro-v2`, `model.TroDocument`): TROs, system boundaries `SYSn`, connections with + `sps_skel-tro-v3`, `model.TroDocument`): TROs, system boundaries `SYSn`, connections with their kind, findings. See `doc/Python_Scripts.md` §4. 2. **`lib/scl_skeleton.py`** (implemented) — reads that TRO-JSON through `TroDocument` (a wrong/renamed key or `items` not matching the catalog stops the run) and emits an `FB_Main` SCL @@ -194,7 +194,6 @@ What is **not** built out yet, relative to the original two-tool design: standalone files) — only `FB_Main` is generated today; - **type-based timing defaults** from `doc/TRO_Typen.md` and the `OVERRIDE_TIMING_JSON` CAD attribute (timings are emitted as `TODO(E-Planung): pruefen` placeholders for now); -- `tro_overrides.py` using the shared `model.Connection` / `model.OpenPoint` classes; - `Component` classes per part type (Separator, Switch, Scanner, VarioDrive, …) on the TRO, driving the per-part SCL emitters in `scl_skeleton.py`. diff --git a/doc/Python_Scripts.md b/doc/Python_Scripts.md index c707687..8e1e526 100644 --- a/doc/Python_Scripts.md +++ b/doc/Python_Scripts.md @@ -60,7 +60,7 @@ Supporting libraries (no CLI of their own, imported by the tools above): name, Siemens FB block, sub-components and counts, color group, CAD marker shape. - **`lib/model.py`** — the shared pydantic model of TRO *instances* and of the - TRO-JSON (`TroBase`, `DerivedTro`, `Connection`, `OpenPoint`, `Analysis`, + TRO-JSON (`TroBase`, `DerivedTro`, `Connection`, `TroCandidate`, `Analysis`, `TroDocument`). Written by `tro_flow.py --json`, read and validated by `scl_skeleton.py`. See §6b. - **`lib/dxf_registration.py`** — derives, verifies and persists the rigid @@ -294,7 +294,7 @@ colors all come from `lib/tro_catalog.py` — nothing is hard-coded here. | `--doc` | — | off | Also write a Markdown report (`_tro_doc.md`) with the TRO list, connections, findings and the full type catalog. | | `--connect` | — | off | Also write the TRO topology as `_tro_connect.ini` (nodes, connections, system boundaries, predecessors/successors per TRO). Pure output, never read back. | | `--true-scale` | — | off | Only with `--use-cords`: draw to scale. By default empty stretches longer than 1500 mm between nodes are shortened (order and neighbourhood are kept). | -| `--json` | — | off | Also write the TRO-JSON `_tro.json` (schema `sps_skel-tro-v2`, model `TroDocument` in `lib/model.py`): TROs, system boundaries (`SYSn`), connections with their kind, findings. Input for `scl_skeleton.py`. | +| `--json` | — | off | Also write the TRO-JSON `_tro.json` (schema `sps_skel-tro-v3`, model `TroDocument` in `lib/model.py`): TROs, system boundaries (`SYSn`), connections with their kind, findings. Input for `scl_skeleton.py`. | TROs, types and connections always come from the layout alone; `cfg/tro_overrides.ini` is not read. Errors in the derivation are fixed in @@ -533,7 +533,7 @@ TRO in a plant, and the whole TRO-JSON, look like. It imports only the catalog. ``` tro_catalog.py type level TroDefinition (items, FB block, color, symbol) ▲ get_tro() -model.py instance level TroBase ─► DerivedTro, Connection, OpenPoint, +model.py instance level TroBase ─► DerivedTro, Connection, TroCandidate, Analysis, TroDocument ▲ tro_flow.py (writes) · scl_skeleton.py (reads) · tro_annotate.py (draws) @@ -544,14 +544,21 @@ tro_flow.py (writes) · scl_skeleton.py (reads) · tro_annotate.py (draws) | `TroBase` | Core of every TRO: `id`, `type`, `confidence`, `separators`, `predecessors`, `successors`, `position` (`{x,y,z}` in JSON). `definition()`, `fb_block` (JSON `fbType`) and `items` are **read from the catalog** on every access, never stored — so they follow a type change and cannot go stale. | | `DerivedTro` | `TroBase` + what `tro_flow.py` knows from the derivation: `label`, `hosts`, `scanners`, `reason`, `colocated`, `positionFrom`, `positionPoints`. Also used for the system boundaries `SYSn`. | | `Connection` | Directed edge `from` → `to` with its `kind` (transfer / umlauf / weiche / unbestimmt / normal / bahn). | -| `OpenPoint` | A reported spot that is not a TRO yet (empty in the JSON for now; meant to replace the class of the same name in `tro_overrides.py`). | +| `TroCandidate` | A spot reported in a review where a TRO is **expected** but not derived (formerly `OpenPoint`): `name`, `type` (the expected catalog type), `position`, `belongsTo` (the TRO that holds the part today), `note`. Not a TRO — no separators, no connections, no SCL. Built by `tro_overrides.py` from `[.openpoint.]`; empty in the TRO-JSON for now. | | `Analysis` | Working result of `tro_flow.analyse_tros` (moved here from `tro_flow.py`). | -| `TroDocument` | The TRO-JSON: `schema` (`sps_skel-tro-v2`), `source`, `created`, `note`, `tros`, `boundaries`, `connections`, `openPoints`, `findings`. `from_analysis()`, `to_json()`, `from_json()`. | +| `TroDocument` | The TRO-JSON: `schema` (`sps_skel-tro-v3`), `source`, `created`, `note`, `tros`, `boundaries`, `connections`, `candidates`, `findings`. `from_analysis()`, `to_json()`, `from_json()`. | + +`TroBase` and `TroCandidate` share the `position` handling (JSON `{x,y,z}`, +`x`/`y`/`z`, `describe_position()`) through the private mixin `_Positioned`. +The same data has the same field name in every class, including +`tro_overrides.py`: `tro_id`, `type_name`, `separators`, `scanners`, `hosts`, +`label`, `reason`, `confidence`, `position`. All models validate on creation and assignment and reject unknown keys (`extra="forbid"`). JSON keys keep the camelCase names of the TRO-JSON (`id`, -`type`, `fbType`, `openPoints` …); Python code uses the field names -(`tro_id`, `type_name`, `fb_block`, `open_points`) — both are accepted on input. +`type`, `fbType`, `belongsTo` …); Python code uses the field names +(`tro_id`, `type_name`, `fb_block`, `belongs_to`) — both are accepted on input. +Schema v3 differs from v2 only in `candidates` replacing `openPoints`. Tests: `tests/test_model.py` (round trip, catalog-backed fields, rejection of wrong keys/items, SCL rendering from a document) — run with @@ -634,7 +641,13 @@ Three kinds of entry, deliberately different in weight: flow node is primary, the rest stay attached so the linking still sees both sides of a transition. Like splits, this renumbers. - **`openpoint`** — a spot reported in a review that is **not** turned into a - TRO. The structure stays untouched. + TRO. The structure stays untouched. Read as `model.TroCandidate` (`expect` → + `type_name`, `x`/`y`/`z` → `position`, missing `z` = 0). + +In Python the entries are `TroOverride` (`tro_id` *or* `separator`, from +`TRO` / `SEP`), `SplitSpec`/`MergeSpec` (`hosts` is a list; the INI key +stays `host`, comma-separated), `model.Connection` for `[.connections]`, +and `model.TroCandidate` — field names as in `model.py`. Consumer: `material_flow.py --connect` (writes the `[.manual]` section into `connect.ini`). Keep in mind: **TRO IDs are running numbers of one run and diff --git a/doc/SCL_Templates.md b/doc/SCL_Templates.md index a616a69..6aca04c 100644 --- a/doc/SCL_Templates.md +++ b/doc/SCL_Templates.md @@ -12,7 +12,7 @@ It is written so that it fits the existing main path unchanged: ``` export.csv ─► tro_flow --json ─► _tro.json ─► scl_skeleton ─► _FB_Main.scl (dxfmakros (derive) (TroDocument, (render) (skeleton with - EXPORTCSV) sps_skel-tro-v2) TODO(E-Planung)) + EXPORTCSV) sps_skel-tro-v3) TODO(E-Planung)) ``` Nothing upstream of `scl_skeleton` changes. The TRO-JSON stays the only input, and the @@ -451,7 +451,7 @@ bin\scl_skeleton.bat --json _tro.json [--templates [DIR]] [--strict] [--na | File | Where | Who writes it | |---|---|---| | `.csv` | `%SKEL_DATA%` | dxfmakros `EXPORTCSV` | -| `_tro.json` | `%SKEL_RESULTS%` | `tro_flow --json` (schema `sps_skel-tro-v2`) | +| `_tro.json` | `%SKEL_RESULTS%` | `tro_flow --json` (schema `sps_skel-tro-v3`) | | `scl_naming.ini`, `_scl_naming.ini` | `%SKEL_CFG%` | you, once per project (optional) | | `lib/scl_templates/*.scl.j2` | repo | maintained with the PLC engineers | diff --git a/doc/updated_mubea/export_tro.json b/doc/updated_mubea/export_tro.json index b0fabc1..e28700e 100644 --- a/doc/updated_mubea/export_tro.json +++ b/doc/updated_mubea/export_tro.json @@ -477,5 +477,5 @@ "to": "TRO05" } ], - "openPoints": [] + "candidates": [] } diff --git a/lib/material_flow.py b/lib/material_flow.py index 7bbaabd..37b9b76 100644 --- a/lib/material_flow.py +++ b/lib/material_flow.py @@ -2349,7 +2349,7 @@ def render_connect_ini(graph: Graph, section: str = "Anlage", overrides=None) -> add("") if overrides is not None and (overrides.tros or overrides.splits - or overrides.open_points): + or overrides.candidates): add(f"[{section}.manual]") add("; Vor Ort bestaetigt, im CSV-Export nicht enthalten.") add(f"; Quelle: {overrides.source}") @@ -2358,16 +2358,16 @@ def render_connect_ini(graph: Graph, section: str = "Anlage", overrides=None) -> for section_key, override in sorted(overrides.tros.items()): if not override.type_name: continue - wie = f"Separator {override.key}" if override.by_separator else override.key + wie = f"Separator {override.separator}" if override.separator else override.tro_id add(f"{section_key} = {override.type_name} | {wie} | {override.reason}") for spec in overrides.splits: add(f"{spec.name} = NEU {spec.type_name} " f"| Separator {', '.join(spec.separators)}" + (f" + Scanner {', '.join(spec.scanners)}" if spec.scanners else "") - + (f" | Bahn {spec.host}" if spec.host else "") + + (f" | Bahn {', '.join(spec.hosts)}" if spec.hosts else "") + f" | {spec.reason}") - for point in overrides.open_points: - add(f"{point.name} = OFFEN {point.expect or '?'} " + for point in overrides.candidates: + add(f"{point.name} = OFFEN {point.type_name or '?'} " f"| x={point.x:.2f} y={point.y:.2f} " f"| heute Teil von {point.belongs_to or '-'} " f"| {point.note}") diff --git a/lib/model.py b/lib/model.py index 40827f7..89ff30a 100644 --- a/lib/model.py +++ b/lib/model.py @@ -19,20 +19,23 @@ Klassen: DerivedTro TroBase + was tro_flow bei der Ableitung zusaetzlich weiss (Hosts, Scanner, Begruendung, Positionsquelle ...) Connection eine gerichtete Verbindung zwischen zwei TROs/Stationen - OpenPoint eine gemeldete Stelle, die noch kein TRO ist + TroCandidate eine im Review gemeldete Stelle, an der ein TRO erwartet + wird, die aber (noch) keiner ist Analysis Arbeitsergebnis von tro_flow.analyse_tros TroDocument die TRO-JSON (_tro.json): von tro_flow --json geschrieben, von scl_skeleton gelesen und geprueft In der JSON stehen die Schluessel wie bisher in der TRO-JSON (`id`, `type`, -`fbType`, `openPoints`, ...); im Python-Code heissen die Felder weiter -`tro_id`/`type_name` (Aliase, `populate_by_name`). +`fbType`, `candidates`, ...); im Python-Code heissen die Felder weiter +`tro_id`/`type_name` (Aliase, `populate_by_name`). Gleiche Daten heissen in +allen Klassen gleich (auch in tro_overrides.py): `type_name`, `separators`, +`hosts`, `position`, `reason`, `confidence`. """ from __future__ import annotations from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any from pydantic import ( BaseModel, @@ -51,14 +54,15 @@ __all__ = [ "TroBase", "DerivedTro", "Connection", - "OpenPoint", + "TroCandidate", "Analysis", "TroDocument", ] # Kennung der TRO-JSON. v1 war die Ausgabe von tro_extract (aus der Zeichnung), -# v2 die von tro_flow --json (aus dem CSV, ueber dieses Modell). -SCHEMA = "sps_skel-tro-v2" +# v2 die von tro_flow --json (aus dem CSV, ueber dieses Modell), v3 wie v2 mit +# `candidates` statt `openPoints`. +SCHEMA = "sps_skel-tro-v3" # Gemeinsame Einstellungen: Pruefung bei Erzeugung und Zuweisung, unbekannte # Schluessel sind ein Fehler (ein umbenannter JSON-Schluessel faellt auf statt @@ -71,7 +75,48 @@ _CONFIG = ConfigDict( ) -class TroBase(BaseModel): +class _Positioned(BaseModel): + """ + Anlagenkoordinate (mm) als Feld ``position``, in der JSON {"x", "y", "z"}. + + Das Feld selbst deklariert jede Unterklasse, damit es dort in der + JSON-Reihenfolge an seiner Stelle steht. + """ + + if TYPE_CHECKING: + position: Any + + @field_validator("position", mode="before", check_fields=False) + @classmethod + def _position_from_json(cls, value: Any) -> Any: + if isinstance(value, dict): + return (value["x"], value["y"], value.get("z", 0.0)) + return value + + @field_serializer("position", when_used="json-unless-none", check_fields=False) + def _position_to_json(self, value: tuple[float, float, float]) -> dict[str, float]: + return {"x": round(value[0], 2), "y": round(value[1], 2), "z": round(value[2], 2)} + + @property + def x(self) -> float | None: + return self.position[0] if self.position else None + + @property + def y(self) -> float | None: + return self.position[1] if self.position else None + + @property + def z(self) -> float | None: + return self.position[2] if self.position else None + + def describe_position(self) -> str: + """Koordinate als Text, z. B. '1030, 4700'.""" + if not self.position: + return "-" + return f"{self.position[0]:.0f}, {self.position[1]:.0f}" + + +class TroBase(_Positioned): """ Kern eines TRO (Instanz in einer Anlage), unabhaengig von seiner Quelle. @@ -145,37 +190,6 @@ class TroBase(BaseModel): ) return data - # -- Position ------------------------------------------------------------ - - @field_validator("position", mode="before") - @classmethod - def _position_from_json(cls, value: Any) -> Any: - if isinstance(value, dict): - return (value["x"], value["y"], value.get("z", 0.0)) - return value - - @field_serializer("position", when_used="json-unless-none") - def _position_to_json(self, value: tuple[float, float, float]) -> dict[str, float]: - return {"x": round(value[0], 2), "y": round(value[1], 2), "z": round(value[2], 2)} - - @property - def x(self) -> float | None: - return self.position[0] if self.position else None - - @property - def y(self) -> float | None: - return self.position[1] if self.position else None - - @property - def z(self) -> float | None: - return self.position[2] if self.position else None - - def describe_position(self) -> str: - """Koordinate als Text, z. B. '1030, 4700'.""" - if not self.position: - return "-" - return f"{self.position[0]:.0f}, {self.position[1]:.0f}" - class DerivedTro(TroBase): """ @@ -210,22 +224,23 @@ class Connection(BaseModel): kind: str = "" -class OpenPoint(BaseModel): - """Eine im Review gemeldete Stelle, die noch kein eigener TRO ist.""" +class TroCandidate(_Positioned): + """ + Eine im Review gemeldete Stelle, an der ein TRO erwartet wird. + + Kein TRO: keine Separatoren, keine Verbindungen, keine SCL. Das Bauteil + steckt heute in einem anderen TRO (``belongs_to``). ``type_name`` ist der + hier erwartete Katalogtyp. + """ model_config = _CONFIG name: str - x: float - y: float - z: float | None = None - expect: str = "" + type_name: str = Field(default="", alias="type") + position: tuple[float, float, float] belongs_to: str = Field(default="", alias="belongsTo") note: str = "" - def describe_position(self) -> str: - return f"{self.x:.0f}, {self.y:.0f}" - class Analysis(BaseModel): """Ergebnis von tro_flow.analyse_tros: TROs, Kanten, Befunde.""" @@ -272,7 +287,7 @@ class TroDocument(BaseModel): # Systemgrenzen (SYSn): Materialein-/ausgang, kein TRO - keine SCL boundaries: list[DerivedTro] = Field(default_factory=list) connections: list[Connection] = Field(default_factory=list) - open_points: list[OpenPoint] = Field(default_factory=list, alias="openPoints") + candidates: list[TroCandidate] = Field(default_factory=list) findings: list[str] = Field(default_factory=list) @classmethod diff --git a/lib/tro_overrides.py b/lib/tro_overrides.py index 77503ba..094c7b9 100644 --- a/lib/tro_overrides.py +++ b/lib/tro_overrides.py @@ -26,15 +26,19 @@ mehrere Anlagen in einer Datei stehen koennen): reason = ... Begruendung (kommt in die Doku) confidence = bestaetigt Vertrauensangabe (Default: bestaetigt) - [.openpoint.] offener Punkt - noch KEIN TRO + [.openpoint.] offener Punkt - noch KEIN TRO (model.TroCandidate) x = 529.95 Anlagenkoordinate in mm y = -3895.27 z = 1468 - expect = 1Sep1Swi hier erwarteter TRO-Typ + 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 @@ -44,6 +48,7 @@ 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" @@ -63,16 +68,17 @@ class TroOverride: wegfallen; die Separator-Nummer aus dem CSV-Export tut das nicht. """ - key: str - by_separator: bool = False + # 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.by_separator: - return self.key in tro.separators - return self.key == tro.tro_id + if self.separator: + return self.separator in tro.separators + return self.tro_id == tro.tro_id @dataclass @@ -90,21 +96,12 @@ class SplitSpec: type_name: str separators: list[str] = field(default_factory=list) scanners: list[str] = field(default_factory=list) - host: str = "" + hosts: list[str] = field(default_factory=list) label: str = "" reason: str = "" confidence: str = DEFAULT_CONFIDENCE -@dataclass -class ConnectionSpec: - """Eine vor Ort bestaetigte Verbindung zwischen zwei TROs.""" - - src: str - dst: str - kind: str = "" - - @dataclass class MergeSpec: """Mehrere abgeleitete TROs zu einem Steuerobjekt zusammenfassen. @@ -119,28 +116,12 @@ class MergeSpec: name: str type_name: str separators: list[str] = field(default_factory=list) - host: str = "" + hosts: list[str] = field(default_factory=list) label: str = "" reason: str = "" confidence: str = DEFAULT_CONFIDENCE -@dataclass -class OpenPoint: - """Eine im Review gemeldete Stelle, die noch kein eigener TRO ist.""" - - name: str - x: float - y: float - z: float | None = None - expect: str = "" - belongs_to: str = "" - note: str = "" - - def describe_position(self) -> str: - return f"{self.x:.0f}, {self.y:.0f}" - - @dataclass class Overrides: """Alle Korrekturen zu einer CSV-Datei.""" @@ -150,15 +131,15 @@ class Overrides: splits: list[SplitSpec] = field(default_factory=list) merges: list[MergeSpec] = field(default_factory=list) # Gesetzte Topologie: ersetzt die abgeleiteten Kanten vollstaendig - connections: list[ConnectionSpec] = field(default_factory=list) - open_points: list[OpenPoint] = field(default_factory=list) + 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.open_points) + or self.candidates) def override_path(cfg_dir: Path | None = None) -> Path: @@ -179,6 +160,10 @@ def _as_float(raw: str, where: str, warnings: list[str]) -> float | None: 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) @@ -205,7 +190,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides: ) continue result.connections.append( - ConnectionSpec(src=src, dst=dst, kind=rest.strip()) + Connection(src=src, dst=dst, kind=rest.strip()) ) continue @@ -230,8 +215,8 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides: key = name.upper() by_sep = key.startswith("SEP") result.tros[key] = TroOverride( - key=key[3:] if by_sep else key, - by_separator=by_sep, + 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, @@ -244,7 +229,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides: f"- neuer TRO nicht angelegt" ) continue - seps = [s.strip() for s in values.get("separators", "").split(",") if s.strip()] + seps = _as_list(values.get("separators", "")) if not seps: result.warnings.append( f"[{section}]: 'separators' fehlt - ohne Separator kein TRO" @@ -255,9 +240,8 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides: name=name, type_name=type_name, separators=seps, - scanners=[s.strip() for s in values.get("scanners", "").split(",") - if s.strip()], - host=values.get("host", "").strip(), + 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, @@ -271,7 +255,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides: f"- Zusammenfassung nicht durchgefuehrt" ) continue - seps = [s.strip() for s in values.get("separators", "").split(",") if s.strip()] + seps = _as_list(values.get("separators", "")) if len(seps) < 2: result.warnings.append( f"[{section}]: 'separators' braucht mindestens zwei Nummern" @@ -282,7 +266,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides: name=name, type_name=type_name, separators=seps, - host=values.get("host", "").strip(), + 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, @@ -294,30 +278,27 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides: if x is None or y is None: continue z_raw = values.get("z", "").strip() - expect = values.get("expect", "").strip() - if expect and get_tro(expect) is None: + 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 '{expect}' steht nicht im " + f"[{section}]: erwarteter Typ '{type_name}' steht nicht im " f"TRO-Katalog" ) - result.open_points.append( - OpenPoint( + result.candidates.append( + TroCandidate( name=name, - x=x, - y=y, - z=_as_float(z_raw, f"[{section}] z", result.warnings) - if z_raw - else None, - expect=expect, + 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' oder " - f"'openpoint'" + f"[{section}]: unbekannte Art '{kind}' - erwartet 'tro', 'split', " + f"'merge' oder 'openpoint'" ) - result.open_points.sort(key=lambda p: p.name) + result.candidates.sort(key=lambda p: p.name) return result diff --git a/tests/fixtures/scl/HundM05_tro.json b/tests/fixtures/scl/HundM05_tro.json index 18745c4..433cd1d 100644 --- a/tests/fixtures/scl/HundM05_tro.json +++ b/tests/fixtures/scl/HundM05_tro.json @@ -1,5 +1,5 @@ { - "schema": "sps_skel-tro-v2", + "schema": "sps_skel-tro-v3", "source": "HundM05.csv", "created": "2026-10-09 11:30", "note": "Aus dem CSV-Export abgeleitet (lib/tro_flow.py) - die Zeichnung ist nur Ansicht, keine Datenquelle.", @@ -917,7 +917,7 @@ "kind": "bahn" } ], - "openPoints": [], + "candidates": [], "findings": [ "Scanner 0019 'Scanner :1': montiert an 0006-L, liest aber an Separator 0018 auf 0005", "TRO16 (VF_5): Scanner 0019 am Separator - als 1Sep eingestuft. Pruefen, ob 1Sep_SSCC (SSCC-Scanner, Endmessung, WCS-Telegramm) erforderlich ist; das ist aus dem mechanischen Layout nicht erkennbar.", diff --git a/tests/fixtures/scl/mubea_tro.json b/tests/fixtures/scl/mubea_tro.json index 9273618..9d4247d 100644 --- a/tests/fixtures/scl/mubea_tro.json +++ b/tests/fixtures/scl/mubea_tro.json @@ -1,5 +1,5 @@ { - "schema": "sps_skel-tro-v2", + "schema": "sps_skel-tro-v3", "source": "mubea.csv", "created": "2026-10-09 11:38", "note": "Aus dem CSV-Export abgeleitet (lib/tro_flow.py) - die Zeichnung ist nur Ansicht, keine Datenquelle.", @@ -888,7 +888,7 @@ "kind": "normal" } ], - "openPoints": [], + "candidates": [], "findings": [ "Kreisel2: Ausschleuselement 0057 und ILS Weiche 0125 benachbart - als 2Sep2Swi zusammengefasst (FB_ILS_MTRO_2Sep2Swi fehlt noch in der ILSLib)", "Scanner 0008 'Scanner :5': montiert an 0010, liest aber an Separator 0042 auf 0010-R", diff --git a/tests/test_model.py b/tests/test_model.py index d389741..6508830 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -6,7 +6,7 @@ import json import pytest from pydantic import ValidationError -from model import SCHEMA, Analysis, Connection, DerivedTro, TroDocument +from model import SCHEMA, Analysis, Connection, DerivedTro, TroCandidate, TroDocument from scl_skeleton import render @@ -64,7 +64,18 @@ def test_json_uses_tro_json_keys(document): assert first["items"] == {"Separator": 1, "Switch": 1} assert first["position"] == {"x": 1030.0, "y": 4700.0, "z": 1500.0} assert data["connections"][1] == {"from": "TRO01", "to": "TRO02", "kind": "weiche"} - assert "openPoints" in data + assert data["candidates"] == [] + + +def test_candidate_round_trip(): + candidate = TroCandidate(name="Qualitaetskontrolle", type_name="1Sep1Swi", + position=(-1026.0, 5250.05, 1478.5), belongs_to="TRO09") + data = json.loads(candidate.model_dump_json(by_alias=True)) + assert data["type"] == "1Sep1Swi" + assert data["position"] == {"x": -1026.0, "y": 5250.05, "z": 1478.5} + assert data["belongsTo"] == "TRO09" + assert TroCandidate.model_validate(data) == candidate + assert candidate.describe_position() == "-1026, 5250" def test_items_and_fb_block_follow_the_type():