validate_anlage, stabiler TRO-Schluessel, Verbindungen aus BricsCAD, Zeichnung aus der Anlagen-JSON
- validate_anlage.py (+ bin-Wrapper): Pruefung vor der SCL fuer beide Wege (Struktur, Zuordnungen, Aktualitaet der JSON, Korrekturen ohne Ziel); scl_skeleton ruft sie auf (Warnung, --strict bricht ab) - DerivedTro.key: stabiler Schluessel (ID der ID-Datei); hw_overrides.ini, <csv>_jam_names.ini und tro_overrides.ini greifen ueber ID oder Schluessel, eine Umbenennung in TRO_EDIT laesst sie bestehen; JAM_AREA traegt KEY - --read-tros liest die CONNECTION_ARROW-Bloecke mit -> [<csv>.edit_connections]; tro_flow.apply_connection_edits wendet sie nach link_tros an - tro_annotate --anlage zeichnet Marker/Verbindungen aus der Anlagen-JSON - INI-Korrekturen auch mit BOM lesbar (utf-8-sig) - Ablauf CSV/DXF+TIA -> Anlagen-JSON -> (BricsCAD) -> SCL in doc/Python_Scripts.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -212,7 +212,9 @@ byte — rewrite them (`SKEL_UPDATE_GOLDEN=1`) only for a reviewed, intended cha
|
|||||||
types, rows from `*TRO-SLOTS*`, pick-in-drawing from the XRECORD `SPS_SKEL_PARTS`) edits
|
types, rows from `*TRO-SLOTS*`, pick-in-drawing from the XRECORD `SPS_SKEL_PARTS`) edits
|
||||||
them, and `--read-tros` writes the differences as `edit`/`edit_add` sections into
|
them, and `--read-tros` writes the differences as `edit`/`edit_add` sections into
|
||||||
`cfg/tro_overrides.ini` — the only kinds of that file `tro_flow` applies
|
`cfg/tro_overrides.ini` — the only kinds of that file `tro_flow` applies
|
||||||
(`tro_flow.apply_edits`, keyed on the stable ID-file IDs).
|
(`tro_flow.apply_edits`, keyed on the stable ID-file IDs); since 2026-10-27 also the
|
||||||
|
connection blocks (`edit_connections`, `tro_flow.apply_connection_edits`). With
|
||||||
|
`--anlage` the markers/connections are drawn from the plant JSON itself.
|
||||||
4. **`lib/tro_extract.py`** — **parked**: reads the annotated DXF back into the old
|
4. **`lib/tro_extract.py`** — **parked**: reads the annotated DXF back into the old
|
||||||
`sps_skel-tro-extract-v1` JSON, which `scl_skeleton.py` no longer accepts. Not deleted, not
|
`sps_skel-tro-extract-v1` JSON, which `scl_skeleton.py` no longer accepts. Not deleted, not
|
||||||
extended; if drawing edits (`TRO_INSERT`/`TRO_EDIT`) are ever needed again, it should
|
extended; if drawing edits (`TRO_INSERT`/`TRO_EDIT`) are ever needed again, it should
|
||||||
@@ -248,6 +250,16 @@ byte — rewrite them (`SKEL_UPDATE_GOLDEN=1`) only for a reviewed, intended cha
|
|||||||
open/contradicting bindings stay `TODO(E-Planung)`;
|
open/contradicting bindings stay `TODO(E-Planung)`;
|
||||||
inserted names are checked against `<projekt>_hw.json` (`--strict` aborts). Templates are
|
inserted names are checked against `<projekt>_hw.json` (`--strict` aborts). Templates are
|
||||||
unchanged. See `doc/Python_Scripts.md` §5b.
|
unchanged. See `doc/Python_Scripts.md` §5b.
|
||||||
|
9. **`lib/validate_anlage.py`** (implemented 2026-10-27) — the gate before SCL for both paths
|
||||||
|
(no changes / after BricsCAD edits): structure, bindings, freshness (plant JSON older than
|
||||||
|
TRO-JSON/hw.json/hw_overrides → rerun merge), corrections that match nothing. Exit 5 = not
|
||||||
|
ready; `scl_skeleton` calls it (warns, `--strict` aborts). The ordered command list for
|
||||||
|
both paths is in `doc/Python_Scripts.md` §1 "The workflow, step by step".
|
||||||
|
|
||||||
|
Since 2026-10-27 every TRO carries a stable `key` (ID-file ID) next to its display `id`;
|
||||||
|
all corrections (`hw_overrides.ini`, `<csv>_jam_names.ini`, `tro_overrides.ini`) resolve by
|
||||||
|
ID **or** key, so a rename in `TRO_EDIT` keeps them. The plant JSON is never hand-patched —
|
||||||
|
it is regenerated from inputs + these overlays.
|
||||||
|
|
||||||
What is **not** built out yet, relative to the original two-tool design:
|
What is **not** built out yet, relative to the original two-tool design:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
@echo off
|
||||||
|
REM ================================================================
|
||||||
|
REM SPS_SKEL - Anlagen-JSON pruefen, bevor daraus SCL wird
|
||||||
|
REM ================================================================
|
||||||
|
REM Aufruf:
|
||||||
|
REM bin\validate_anlage.bat --json mubea_anlage.json
|
||||||
|
REM bin\validate_anlage.bat --json dematic_merge_anlage.json --strict
|
||||||
|
REM
|
||||||
|
REM Die JSON wird in %SKEL_RESULTS% gesucht. Exit-Code 0 = bereit,
|
||||||
|
REM 5 = nicht bereit (Fehler; mit --strict auch Warnungen).
|
||||||
|
REM ================================================================
|
||||||
|
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
REM Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_LIB, PYTHONPATH ...)
|
||||||
|
call "%~dp0setenv.bat" >nul
|
||||||
|
|
||||||
|
REM Virtuelle Umgebung nutzen, falls vorhanden (pydantic wird benoetigt)
|
||||||
|
if exist "%SPS_SKEL%\.venv\Scripts\activate.bat" (
|
||||||
|
call "%SPS_SKEL%\.venv\Scripts\activate.bat"
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Python-Interpreter bestimmen (py-Launcher bevorzugt, sonst python)
|
||||||
|
set "PY=py"
|
||||||
|
py --version >nul 2>&1 || set "PY=python"
|
||||||
|
|
||||||
|
"%PY%" "%SKEL_LIB%\validate_anlage.py" %*
|
||||||
|
set "RC=%ERRORLEVEL%"
|
||||||
|
|
||||||
|
endlocal & exit /b %RC%
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ================================================================
|
||||||
|
# SPS_SKEL - Anlagen-JSON pruefen, bevor daraus SCL wird
|
||||||
|
# ================================================================
|
||||||
|
# Aufruf:
|
||||||
|
# bash bin/validate_anlage.sh --json mubea_anlage.json
|
||||||
|
# bash bin/validate_anlage.sh --json dematic_merge_anlage.json --strict
|
||||||
|
#
|
||||||
|
# Die JSON wird in $SKEL_RESULTS gesucht. Exit-Code 0 = bereit,
|
||||||
|
# 5 = nicht bereit (Fehler; mit --strict auch Warnungen).
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_LIB, PYTHONPATH ...)
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$SCRIPT_DIR/setenv.sh" >/dev/null
|
||||||
|
|
||||||
|
# Virtuelle Umgebung nutzen, falls vorhanden (pydantic wird benoetigt)
|
||||||
|
if [ -f "$SPS_SKEL/.venv/bin/activate" ]; then
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$SPS_SKEL/.venv/bin/activate"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Python-Interpreter bestimmen
|
||||||
|
PY="python3"
|
||||||
|
command -v python3 >/dev/null 2>&1 || PY="python"
|
||||||
|
|
||||||
|
exec "$PY" "$SKEL_LIB/validate_anlage.py" "$@"
|
||||||
+90
-3
@@ -63,7 +63,40 @@ parked: tro_extract.py drawing ──► JSON (not extended; see §5a)
|
|||||||
|
|
||||||
The drawing is a **view** for checking the result, not a data source: the
|
The drawing is a **view** for checking the result, not a data source: the
|
||||||
TRO-JSON is written by `tro_flow.py --json` straight from the CSV, and
|
TRO-JSON is written by `tro_flow.py --json` straight from the CSV, and
|
||||||
`tro_annotate.py` derives exactly the same TROs to draw them.
|
`tro_annotate.py` derives exactly the same TROs to draw them — with
|
||||||
|
`--anlage` it draws the plant JSON itself (since 2026-10-27).
|
||||||
|
|
||||||
|
### The workflow, step by step (since 2026-10-27)
|
||||||
|
|
||||||
|
The plant JSON `<name>_anlage.json` is the **unified JSON**: layout (TROs,
|
||||||
|
connections, jam areas) + hardware bindings. It is always **regenerated**;
|
||||||
|
corrections made by people live in INI overlays in `%SKEL_CFG%` that are
|
||||||
|
keyed on stable IDs and survive a new CSV export:
|
||||||
|
|
||||||
|
| Overlay | Written by | Applied by | Keyed on |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `tro_overrides.ini` `edit`/`edit_add`/`edit_connections` | `tro_annotate --read-tros` | `tro_flow` (and `tro_annotate`) | TRO key (`DerivedTro.key`) |
|
||||||
|
| `<csv>_jam_names.ini` | `tro_annotate --read-jams` | `scl_skeleton`, `tro_annotate` | `<TRO key>.<entry>` |
|
||||||
|
| `hw_overrides.ini` | by hand | `merge_hw_layout` | TRO ID **or** key |
|
||||||
|
|
||||||
|
```
|
||||||
|
1 tro_flow --file <csv> --json CSV -> <csv>_tro.json
|
||||||
|
2 hw_import --tia <x>_TIA.xlsx --positions|--dxf E-plan + TIA -> <projekt>_hw.json
|
||||||
|
3 merge_hw_layout --tro <csv>_tro.json --hw <projekt>_hw.json -> <csv>_anlage.json
|
||||||
|
4 validate_anlage --json <csv>_anlage.json ready for SCL? (§5f)
|
||||||
|
|
||||||
|
Path A (no changes): scl_skeleton --json <csv>_anlage.json [--templates] [--strict]
|
||||||
|
Path B (changes): tro_annotate --file <csv> --dxf <dwg>.dxf --flow --anlage <csv>_anlage.json
|
||||||
|
... edit in BricsCAD (TRO_EDIT, TRO_INSERT, JAM_EDIT,
|
||||||
|
CONNECTION_INSERT/_EDIT, delete markers/arrow blocks), save as DXF
|
||||||
|
tro_annotate --file <csv> --read-tros -> tro_overrides.ini
|
||||||
|
tro_annotate --file <csv> --read-jams -> <csv>_jam_names.ini
|
||||||
|
then steps 1, 3, 4 again, then Path A
|
||||||
|
```
|
||||||
|
|
||||||
|
`scl_skeleton` runs the step-4 check itself (warns; `--strict` aborts on
|
||||||
|
errors), so an outdated plant JSON — e.g. step 3 forgotten after step 1 — is
|
||||||
|
reported, not silently turned into SCL.
|
||||||
|
|
||||||
- `material_flow.py` models the material flow of the **mechanical** objects only
|
- `material_flow.py` models the material flow of the **mechanical** objects only
|
||||||
(Gefällestrecke, Strecke, Kreisel).
|
(Gefällestrecke, Strecke, Kreisel).
|
||||||
@@ -467,7 +500,36 @@ falls back to the nearest part of the same kind at that spot. The sections
|
|||||||
replace all earlier `edit`/`edit_add` sections of the CSV in
|
replace all earlier `edit`/`edit_add` sections of the CSV in
|
||||||
`tro_overrides.ini` (a `reason =` added by hand is kept); the rest of the file
|
`tro_overrides.ini` (a `reason =` added by hand is kept); the rest of the file
|
||||||
stays line for line. The next `tro_flow` / `tro_annotate` run applies them
|
stays line for line. The next `tro_flow` / `tro_annotate` run applies them
|
||||||
(§4); a re-read of the new drawing gives the same sections. `--check` only
|
(§4); a re-read of the new drawing gives the same sections.
|
||||||
|
|
||||||
|
**Connections (since 2026-10-27).** `--read-tros` also reads every
|
||||||
|
`CONNECTION_ARROW` block. Each end is matched to a marker of the same drawing
|
||||||
|
by its visible name (`FROM_TRO`/`TO_TRO`), else by the internal number
|
||||||
|
(`FROM_UID`/`TO_UID`; older dxfmakros blocks `FROM_ID`/`TO_ID`) — so arrows
|
||||||
|
still point right after a marker was renamed. The arrows are compared with the
|
||||||
|
derivation *including* the TRO corrections (edges of a new or edited TRO do not
|
||||||
|
count as hand-drawn); the differences go to `[<csv>.edit_connections]` as
|
||||||
|
`+ KEY_A -> KEY_B | kind` / `- KEY_A -> KEY_B`, ends as stable keys.
|
||||||
|
`tro_flow.apply_connection_edits` applies them after `link_tros` and resets
|
||||||
|
predecessors/successors, so the jam areas follow. Station arrows carry no
|
||||||
|
block and stay as derived; an arrow is deleted by deleting its block. Before
|
||||||
|
this, `CONNECTION_INSERT`/`_EDIT` in dxfmakros wrote attribute names the
|
||||||
|
blocks did not have (`ID`/`FROM_ID`/`TO_ID`, marker name from `TRO_ID`) — fixed
|
||||||
|
there on the same day.
|
||||||
|
|
||||||
|
**Stable key (since 2026-10-27).** Every TRO in the TRO-JSON carries `key`,
|
||||||
|
its ID in `<csv>_tro_ids.ini` (or, for a TRO added in BricsCAD, its own ID).
|
||||||
|
It differs from `id` only after a rename in `TRO_EDIT`. All corrections are
|
||||||
|
resolved by ID **or** key (`TroDocument.resolve`, `scl_context.jam_override`),
|
||||||
|
so a rename does not orphan `hw_overrides.ini` or `<csv>_jam_names.ini`
|
||||||
|
entries. Jam blocks carry a hidden `KEY` (`<TRO key>.<entry>`) that
|
||||||
|
`--read-jams` writes; an entry under the old display ID is moved to the key.
|
||||||
|
|
||||||
|
**With `--anlage` the drawing shows the plant JSON** (since 2026-10-27):
|
||||||
|
markers, connections and stations come from `anlage.layout`
|
||||||
|
(`analysis_from_document`), i.e. exactly what becomes SCL. The CSV is still
|
||||||
|
read for the registration and the parts table; if its derivation differs from
|
||||||
|
the JSON, a warning says the JSON is probably outdated. `--check` only
|
||||||
reports. If the drawing has no marker of the derived TROs (wrong file, or
|
reports. If the drawing has no marker of the derived TROs (wrong file, or
|
||||||
annotated before the `KEY`/`SLOTS` attributes), nothing is written —
|
annotated before the `KEY`/`SLOTS` attributes), nothing is written —
|
||||||
re-annotating an old drawing adds the missing attributes to the existing
|
re-annotating an old drawing adds the missing attributes to the existing
|
||||||
@@ -496,7 +558,7 @@ verified on every run, and cached per CSV/DXF filename pair in
|
|||||||
| `--emit-lisp` | — | off | Regenerate `cad/tro_types.lsp` for the `TRO_INSERT`/`TRO_EDIT` dialog — type list (`*TRO-TYPES*`), slots per type with the storage-line default (`*TRO-SLOTS*`), part kinds per slot (`*TRO-SLOT-KINDS*`) — from `tro_catalog.py`/`model.py` and exit — no `--dxf` needed. Written under `%DXFM_LISP%` if set (next to the `dxfmakros` SSG_LIB modules that provide the dialogs), otherwise under `<SPS_SKEL>/cad`. |
|
| `--emit-lisp` | — | off | Regenerate `cad/tro_types.lsp` for the `TRO_INSERT`/`TRO_EDIT` dialog — type list (`*TRO-TYPES*`), slots per type with the storage-line default (`*TRO-SLOTS*`), part kinds per slot (`*TRO-SLOT-KINDS*`) — from `tro_catalog.py`/`model.py` and exit — no `--dxf` needed. Written under `%DXFM_LISP%` if set (next to the `dxfmakros` SSG_LIB modules that provide the dialogs), otherwise under `<SPS_SKEL>/cad`. |
|
||||||
| `--anlage` | `NAME` | — | Plant JSON (`<csv>_anlage.json` from `merge_hw_layout`, in `%SKEL_RESULTS%` or a path): also draw the **jam areas** (see below). Must belong to the same CSV. |
|
| `--anlage` | `NAME` | — | Plant JSON (`<csv>_anlage.json` from `merge_hw_layout`, in `%SKEL_RESULTS%` or a path): also draw the **jam areas** (see below). Must belong to the same CSV. |
|
||||||
| `--read-jams` | `[DXF]` | `<csv>_annotated.dxf` | Read the jam-area numbers edited in BricsCAD back from the annotated drawing (saved as DXF) into `%SKEL_CFG%\<csv>_jam_names.ini`, then exit — no `--dxf` needed. |
|
| `--read-jams` | `[DXF]` | `<csv>_annotated.dxf` | Read the jam-area numbers edited in BricsCAD back from the annotated drawing (saved as DXF) into `%SKEL_CFG%\<csv>_jam_names.ini`, then exit — no `--dxf` needed. |
|
||||||
| `--read-tros` | `[DXF]` | `<csv>_annotated.dxf` | Read the TRO markers edited in BricsCAD (`TRO_EDIT`/`TRO_INSERT`, deleted markers) back from the annotated drawing (saved as DXF) into the `edit`/`edit_add` sections of `%SKEL_CFG%\tro_overrides.ini`, then exit — no `--dxf` needed. With `--check` only reported. |
|
| `--read-tros` | `[DXF]` | `<csv>_annotated.dxf` | Read the TRO markers edited in BricsCAD (`TRO_EDIT`/`TRO_INSERT`, deleted markers) and the connection blocks (`CONNECTION_INSERT`/`_EDIT`, deleted blocks) back from the annotated drawing (saved as DXF) into the `edit`/`edit_add`/`edit_connections` sections of `%SKEL_CFG%\tro_overrides.ini`, then exit — no `--dxf` needed. With `--check` only reported. |
|
||||||
|
|
||||||
**Jam areas (`--anlage`).** One block `JAM_AREA` per jam area of the plant
|
**Jam areas (`--anlage`).** One block `JAM_AREA` per jam area of the plant
|
||||||
JSON on layer `TRO_JAM`: an upright grey rectangle with the name written
|
JSON on layer `TRO_JAM`: an upright grey rectangle with the name written
|
||||||
@@ -1007,6 +1069,31 @@ fingerprint of the stored registration, CLI end to end with a real offset,
|
|||||||
merged drawing without registration, two drive TROs on one conveyor, Dematic
|
merged drawing without registration, two drive TROs on one conveyor, Dematic
|
||||||
rules file).
|
rules file).
|
||||||
|
|
||||||
|
## 5f. `validate_anlage.py` — is the plant JSON ready for SCL?
|
||||||
|
|
||||||
|
One check for both paths of the workflow (§1): directly after
|
||||||
|
`merge_hw_layout` and after BricsCAD corrections have been read back and the
|
||||||
|
JSON rebuilt. `scl_skeleton` calls it before writing (findings as warnings;
|
||||||
|
with `--strict`, errors stop the run). Works on a plant JSON and, reduced, on
|
||||||
|
a TRO-JSON.
|
||||||
|
|
||||||
|
| Area | ERROR (no SCL like this) | WARNING (SCL with open spots) |
|
||||||
|
|---|---|---|
|
||||||
|
| Structure | duplicate TRO-ID, type not in catalog, connection or jam area to an unknown TRO, separator in two TROs | separator slot without layout part, TRO without connection, TRO not reachable from the material entry |
|
||||||
|
| Bindings | binding to an unknown TRO/slot, status `widerspruch`, BMK missing in `<projekt>_hw.json` | open mandatory roles (counted per role) |
|
||||||
|
| Freshness | plant JSON older than TRO-JSON, hardware planning or `hw_overrides.ini` (only if it has sections for this CSV) → run `merge_hw_layout` again | `tro_overrides.ini` with BricsCAD corrections newer than the TRO-JSON → run `tro_flow --json` again |
|
||||||
|
| Corrections | — | entries in `hw_overrides.ini` / `<csv>_jam_names.ini` that match no TRO/jam area, neither by ID nor by key |
|
||||||
|
|
||||||
|
| Switch | Argument | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--json` | `NAME` | — | Plant JSON (or TRO-JSON) in `%SKEL_RESULTS%`, or a path. |
|
||||||
|
| `--hw` | `NAME` | `hwFile` of the plant JSON | Hardware planning for the BMK check. |
|
||||||
|
| `--strict` | — | off | Warnings also make the JSON "not ready". |
|
||||||
|
|
||||||
|
**Exit codes:** `0` ready · `1` input error · `5` not ready.
|
||||||
|
Mubea on 2026-10-27: the plant JSON in `results/` was reported older than the
|
||||||
|
TRO-JSON — exactly the TRO18/TRO19 mismatch `tro_annotate --anlage` showed.
|
||||||
|
|
||||||
## 6. `tro_catalog.py` — TRO type catalog (library, no CLI)
|
## 6. `tro_catalog.py` — TRO type catalog (library, no CLI)
|
||||||
|
|
||||||
The single source of truth for everything needed to generate or draw a TRO
|
The single source of truth for everything needed to generate or draw a TRO
|
||||||
|
|||||||
@@ -616,7 +616,8 @@ def load_overrides(path: Path, layout_source: str) -> Overrides:
|
|||||||
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"),
|
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"),
|
||||||
interpolation=None)
|
interpolation=None)
|
||||||
parser.optionxform = str
|
parser.optionxform = str
|
||||||
with path.open(encoding="utf-8") as fh:
|
# utf-8-sig: von Hand (Editor, PowerShell) gespeichert oft mit BOM
|
||||||
|
with path.open(encoding="utf-8-sig") as fh:
|
||||||
parser.read_file(fh)
|
parser.read_file(fh)
|
||||||
section = f"{layout_source}.bindings"
|
section = f"{layout_source}.bindings"
|
||||||
if parser.has_section(section):
|
if parser.has_section(section):
|
||||||
@@ -639,7 +640,10 @@ def _apply_overrides(doc: TroDocument, bindings: list[Binding],
|
|||||||
hw: HardwareDocument, warnings: list[str]) -> list[Binding]:
|
hw: HardwareDocument, warnings: list[str]) -> list[Binding]:
|
||||||
devices = hw.by_bmk()
|
devices = hw.by_bmk()
|
||||||
slots = {(t.tro_id, s.slot): s for t in doc.tros for s in t.slots}
|
slots = {(t.tro_id, s.slot): s for t in doc.tros for s in t.slots}
|
||||||
for (tro, slot, role), bmk in overrides.bindings.items():
|
for (name, slot, role), bmk in overrides.bindings.items():
|
||||||
|
# ID oder - nach einer Umbenennung in BricsCAD - der stabile Schluessel
|
||||||
|
found = doc.resolve(name)
|
||||||
|
tro = found.tro_id if found is not None else name
|
||||||
s = slots.get((tro, slot))
|
s = slots.get((tro, slot))
|
||||||
if s is None:
|
if s is None:
|
||||||
warnings.append(f"{OVERRIDES_FILE}: {tro}.{slot} gibt es in der TRO-JSON nicht "
|
warnings.append(f"{OVERRIDES_FILE}: {tro}.{slot} gibt es in der TRO-JSON nicht "
|
||||||
|
|||||||
+23
-1
@@ -332,6 +332,12 @@ class DerivedTro(TroBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
label: str
|
label: str
|
||||||
|
# Stabiler Schluessel: die ID aus <csv>_tro_ids.ini (bzw. die Kennung eines
|
||||||
|
# in BricsCAD angelegten TRO). Weicht von ``tro_id`` ab, wenn der TRO im
|
||||||
|
# Dialog TRO_EDIT umbenannt wurde. Handkorrekturen (hw_overrides.ini,
|
||||||
|
# <csv>_jam_names.ini, tro_overrides.ini) gelten ueber ID *oder* Schluessel,
|
||||||
|
# damit sie eine Umbenennung ueberleben. "" in Dateien vor 2026-10-27.
|
||||||
|
key: str = ""
|
||||||
hosts: list[str]
|
hosts: list[str]
|
||||||
scanners: list[str]
|
scanners: list[str]
|
||||||
# Weichen-Bauteile (Ausschleuselement oder ILS Weiche), Reihenfolge wie die
|
# Weichen-Bauteile (Ausschleuselement oder ILS Weiche), Reihenfolge wie die
|
||||||
@@ -351,6 +357,11 @@ class DerivedTro(TroBase):
|
|||||||
def connected(self) -> bool:
|
def connected(self) -> bool:
|
||||||
return bool(self.successors or self.predecessors or self.colocated)
|
return bool(self.successors or self.predecessors or self.colocated)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stable_key(self) -> str:
|
||||||
|
"""Schluessel fuer Handkorrekturen (``key``, sonst die ID)."""
|
||||||
|
return self.key or self.tro_id
|
||||||
|
|
||||||
|
|
||||||
class Connection(BaseModel):
|
class Connection(BaseModel):
|
||||||
"""Gerichtete Verbindung zwischen zwei TROs/Stationen (JSON: from/to/kind)."""
|
"""Gerichtete Verbindung zwischen zwei TROs/Stationen (JSON: from/to/kind)."""
|
||||||
@@ -451,7 +462,9 @@ class TroDocument(BaseModel):
|
|||||||
traegt und nicht erst das Rendern sie kennt. ``parts`` (Lage der
|
traegt und nicht erst das Rendern sie kennt. ``parts`` (Lage der
|
||||||
Layout-Bauteile) gibt tro_flow aus dem CSV mit.
|
Layout-Bauteile) gibt tro_flow aus dem CSV mit.
|
||||||
"""
|
"""
|
||||||
tros = [t.model_copy(update={"slots": derive_slots(t)}) for t in analysis.tros]
|
tros = [t.model_copy(update={"slots": derive_slots(t),
|
||||||
|
"key": analysis.edit_key(t.tro_id)})
|
||||||
|
for t in analysis.tros]
|
||||||
return cls(
|
return cls(
|
||||||
source=source,
|
source=source,
|
||||||
note=note,
|
note=note,
|
||||||
@@ -490,6 +503,15 @@ class TroDocument(BaseModel):
|
|||||||
return tro
|
return tro
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def resolve(self, name: str) -> DerivedTro | None:
|
||||||
|
"""TRO zu einer ID oder - nach einer Umbenennung - zu seinem Schluessel.
|
||||||
|
|
||||||
|
Die ID geht vor: ein neuer TRO darf die alte Kennung eines umbenannten
|
||||||
|
tragen, dann meint ein Eintrag mit dieser Kennung den neuen.
|
||||||
|
"""
|
||||||
|
return self.by_id(name) or next(
|
||||||
|
(t for t in self.tros if t.key and t.key == name), None)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Anlagen-JSON: Layout + Zuordnungen der Hardwareplanung (merge_hw_layout)
|
# Anlagen-JSON: Layout + Zuordnungen der Hardwareplanung (merge_hw_layout)
|
||||||
|
|||||||
+25
-3
@@ -68,6 +68,8 @@ __all__ = [
|
|||||||
"flow_order",
|
"flow_order",
|
||||||
"JAM_NAMES_SUFFIX",
|
"JAM_NAMES_SUFFIX",
|
||||||
"jam_key",
|
"jam_key",
|
||||||
|
"jam_override",
|
||||||
|
"jam_stable_key",
|
||||||
"jam_label",
|
"jam_label",
|
||||||
"jam_names",
|
"jam_names",
|
||||||
"jam_numbers",
|
"jam_numbers",
|
||||||
@@ -653,6 +655,25 @@ def jam_key(area: JamArea) -> str:
|
|||||||
return f"{area.tro}.{area.entry}"
|
return f"{area.tro}.{area.entry}"
|
||||||
|
|
||||||
|
|
||||||
|
def jam_stable_key(area: JamArea, document: TroDocument) -> str:
|
||||||
|
"""Wie jam_key, aber mit dem stabilen Schluessel des TRO (DerivedTro.key):
|
||||||
|
bleibt gleich, wenn der TRO in BricsCAD umbenannt wird."""
|
||||||
|
tro = document.by_id(area.tro)
|
||||||
|
return f"{tro.stable_key}.{area.entry}" if tro is not None else jam_key(area)
|
||||||
|
|
||||||
|
|
||||||
|
def jam_override(area: JamArea, document: TroDocument,
|
||||||
|
overrides: dict[str, tuple[str, int]] | None) -> tuple[str, int] | None:
|
||||||
|
"""Korrektur eines Staubereichs: ueber den stabilen Schluessel, sonst ueber
|
||||||
|
die aktuelle TRO-ID (Dateien vor 2026-10-27 bzw. ohne Umbenennung)."""
|
||||||
|
if not overrides:
|
||||||
|
return None
|
||||||
|
for key in (jam_stable_key(area, document), jam_key(area)):
|
||||||
|
if key in overrides:
|
||||||
|
return overrides[key]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def jam_label(nr: str, k: int) -> str:
|
def jam_label(nr: str, k: int) -> str:
|
||||||
"""Kurzname fuer Zeichnung und Dialog: Jam0054_3."""
|
"""Kurzname fuer Zeichnung und Dialog: Jam0054_3."""
|
||||||
return f"Jam{nr}_{k}"
|
return f"Jam{nr}_{k}"
|
||||||
@@ -671,7 +692,8 @@ def load_jam_overrides(path: Path) -> dict[str, tuple[str, int]]:
|
|||||||
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"),
|
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"),
|
||||||
interpolation=None)
|
interpolation=None)
|
||||||
parser.optionxform = str
|
parser.optionxform = str
|
||||||
with path.open(encoding="utf-8") as fh:
|
# utf-8-sig: von Hand (Editor, PowerShell) gespeichert oft mit BOM
|
||||||
|
with path.open(encoding="utf-8-sig") as fh:
|
||||||
parser.read_file(fh)
|
parser.read_file(fh)
|
||||||
result: dict[str, tuple[str, int]] = {}
|
result: dict[str, tuple[str, int]] = {}
|
||||||
if parser.has_section("jam"):
|
if parser.has_section("jam"):
|
||||||
@@ -711,8 +733,8 @@ def jam_names(document: TroDocument, geordnet: list[TroBase], hw: HwSource,
|
|||||||
"""
|
"""
|
||||||
nummern = jam_numbers(document, geordnet, hw)
|
nummern = jam_numbers(document, geordnet, hw)
|
||||||
for area in document.jam_areas_or_derived():
|
for area in document.jam_areas_or_derived():
|
||||||
if overrides and jam_key(area) in overrides:
|
if (wert := jam_override(area, document, overrides)) is not None:
|
||||||
nummern[area.jam_id] = overrides[jam_key(area)]
|
nummern[area.jam_id] = wert
|
||||||
return {jam_id: f'"DB_JamArea{nr}".stJam{nr}_{k}'
|
return {jam_id: f'"DB_JamArea{nr}".stJam{nr}_{k}'
|
||||||
for jam_id, (nr, k) in nummern.items()}
|
for jam_id, (nr, k) in nummern.items()}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ from pydantic import BaseModel, ValidationError
|
|||||||
|
|
||||||
from e_planning import EPLAN_SCHEMA, EplanDocument
|
from e_planning import EPLAN_SCHEMA, EplanDocument
|
||||||
from hw_import import HardwareDocument
|
from hw_import import HardwareDocument
|
||||||
|
from validate_anlage import validate
|
||||||
from material_flow import env_dir
|
from material_flow import env_dir
|
||||||
from model import ANLAGE_SCHEMA, SCHEMA, AnlageDocument, TroDocument
|
from model import ANLAGE_SCHEMA, SCHEMA, AnlageDocument, TroDocument
|
||||||
from scl_context import (
|
from scl_context import (
|
||||||
@@ -441,6 +442,20 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
print(f"FEHLER: {json_file} enthaelt keine TROs.", file=sys.stderr)
|
print(f"FEHLER: {json_file} enthaelt keine TROs.", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
# Pruefung vor der SCL (validate_anlage): Fehler brechen nur mit --strict
|
||||||
|
# ab, sonst stehen sie als Warnung im Bericht.
|
||||||
|
pruefung = validate(json_file, hw_file=_in_results(args.hw, results) if args.hw else None,
|
||||||
|
results=json_file.parent)
|
||||||
|
for befund in pruefung.befunde:
|
||||||
|
if befund.stufe == "FEHLER" or args.strict:
|
||||||
|
print(f"{'FEHLER' if args.strict else 'WARNUNG'}: {befund.bereich}: {befund.text}",
|
||||||
|
file=sys.stderr)
|
||||||
|
if args.strict and not pruefung.bereit():
|
||||||
|
print(f" (--strict) {pruefung.anzahl_fehler} Fehler in der Pruefung - keine "
|
||||||
|
f"SCL geschrieben (Details: bin/validate_anlage --json {json_file.name}).",
|
||||||
|
file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
templates = None
|
templates = None
|
||||||
if args.templates is not None:
|
if args.templates is not None:
|
||||||
ordner = Path(args.templates)
|
ordner = Path(args.templates)
|
||||||
@@ -550,6 +565,8 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
print(f"Gegenpruefung = {len(unbekannt)} unbekannte Namen ({hw_file.name})")
|
print(f"Gegenpruefung = {len(unbekannt)} unbekannte Namen ({hw_file.name})")
|
||||||
if jam_overrides:
|
if jam_overrides:
|
||||||
print(f"Staubereiche = {len(jam_overrides)} Korrekturen aus {jam_file}")
|
print(f"Staubereiche = {len(jam_overrides)} Korrekturen aus {jam_file}")
|
||||||
|
print(f"Pruefung = {pruefung.anzahl_fehler} Fehler, {pruefung.anzahl_warnungen} "
|
||||||
|
f"Warnungen (validate_anlage)")
|
||||||
print(f"Offene Stellen = {offen} (Marker '{TODO}')")
|
print(f"Offene Stellen = {offen} (Marker '{TODO}')")
|
||||||
print(f"SCL = {out_file}")
|
print(f"SCL = {out_file}")
|
||||||
print("================================================================")
|
print("================================================================")
|
||||||
|
|||||||
+165
-32
@@ -143,20 +143,29 @@ from model import (
|
|||||||
AnlageDocument,
|
AnlageDocument,
|
||||||
DerivedTro,
|
DerivedTro,
|
||||||
JamArea,
|
JamArea,
|
||||||
|
TroDocument,
|
||||||
derive_slots,
|
derive_slots,
|
||||||
has_lines,
|
has_lines,
|
||||||
slot_kind,
|
slot_kind,
|
||||||
slot_names,
|
slot_names,
|
||||||
)
|
)
|
||||||
from tro_ids import id_problem
|
from tro_ids import id_problem
|
||||||
from tro_overrides import PartRef, TroEdit, load_overrides, override_path, save_edits
|
from tro_overrides import (
|
||||||
|
ConnectionEdit,
|
||||||
|
PartRef,
|
||||||
|
TroEdit,
|
||||||
|
load_overrides,
|
||||||
|
override_path,
|
||||||
|
save_edits,
|
||||||
|
)
|
||||||
from scl_context import (
|
from scl_context import (
|
||||||
JAM_NAMES_SUFFIX,
|
JAM_NAMES_SUFFIX,
|
||||||
HwSource,
|
HwSource,
|
||||||
flow_order,
|
flow_order,
|
||||||
jam_key,
|
|
||||||
jam_label,
|
jam_label,
|
||||||
jam_numbers,
|
jam_numbers,
|
||||||
|
jam_override,
|
||||||
|
jam_stable_key,
|
||||||
load_jam_overrides,
|
load_jam_overrides,
|
||||||
save_jam_overrides,
|
save_jam_overrides,
|
||||||
)
|
)
|
||||||
@@ -180,6 +189,8 @@ JAM_BLOCK = "JAM_AREA"
|
|||||||
PARTS_XRECORD = "SPS_SKEL_PARTS"
|
PARTS_XRECORD = "SPS_SKEL_PARTS"
|
||||||
# Unsichtbare Attribute der Marker (build_symbol)
|
# Unsichtbare Attribute der Marker (build_symbol)
|
||||||
HIDDEN_TAGS = ("UID", "KEY", "ITEMS", "CONFIDENCE", "SLOTS")
|
HIDDEN_TAGS = ("UID", "KEY", "ITEMS", "CONFIDENCE", "SLOTS")
|
||||||
|
# Unsichtbare Attribute der Staubereiche (build_jam_symbol)
|
||||||
|
JAM_HIDDEN_TAGS = ("CONVEYOR", "NUMBER", "TRO", "ENTRY", "AREA", "AUTO", "KEY")
|
||||||
# Anzeige eines Staubereichs ohne Foerderer (Nummer nicht ableitbar)
|
# Anzeige eines Staubereichs ohne Foerderer (Nummer nicht ableitbar)
|
||||||
JAM_OFFEN = "Jam????_?"
|
JAM_OFFEN = "Jam????_?"
|
||||||
|
|
||||||
@@ -854,35 +865,49 @@ def build_jam_symbol(doc, text: float) -> str:
|
|||||||
AREA unsichtbar - Kennung der TRO-JSON (J0039)
|
AREA unsichtbar - Kennung der TRO-JSON (J0039)
|
||||||
AUTO unsichtbar - abgeleitete Nummer NNNN_k; --read-jams schreibt
|
AUTO unsichtbar - abgeleitete Nummer NNNN_k; --read-jams schreibt
|
||||||
nur, was davon abweicht
|
nur, was davon abweicht
|
||||||
TRO + ENTRY ist der stabile Schluessel (scl_context.jam_key).
|
KEY unsichtbar - stabiler Schluessel <TRO-Schluessel>.<Eingang>
|
||||||
|
(scl_context.jam_stable_key), ueberlebt eine Umbenennung
|
||||||
|
KEY (sonst TRO + ENTRY) ist der Schluessel in <csv>_jam_names.ini.
|
||||||
"""
|
"""
|
||||||
|
w, h = text * JAM_HALB_BREITE, text * JAM_HALB_HOEHE
|
||||||
if JAM_BLOCK in doc.blocks:
|
if JAM_BLOCK in doc.blocks:
|
||||||
# Form aus einem frueheren Lauf (liegendes Rechteck) ersetzen; die
|
# Form aus einem frueheren Lauf (liegendes Rechteck) ersetzen; die
|
||||||
# Attributdefinitionen bleiben, damit vorhandene Bloecke gueltig sind.
|
# Attributdefinitionen bleiben, damit vorhandene Bloecke gueltig sind,
|
||||||
|
# fehlende (KEY) kommen dazu.
|
||||||
blk = doc.blocks.get(JAM_BLOCK)
|
blk = doc.blocks.get(JAM_BLOCK)
|
||||||
for e in list(blk):
|
for e in list(blk):
|
||||||
if e.dxftype() != "ATTDEF":
|
if e.dxftype() != "ATTDEF":
|
||||||
blk.delete_entity(e)
|
blk.delete_entity(e)
|
||||||
_jam_form(blk, text)
|
_jam_form(blk, text)
|
||||||
|
vorhanden = set()
|
||||||
for a in blk.query("ATTDEF"):
|
for a in blk.query("ATTDEF"):
|
||||||
|
vorhanden.add(a.dxf.tag.upper())
|
||||||
if a.dxf.tag.upper() == "NAME":
|
if a.dxf.tag.upper() == "NAME":
|
||||||
_jam_name(a, text)
|
_jam_name(a, text)
|
||||||
|
offset = min((a.dxf.insert.y for a in blk.query("ATTDEF")), default=-h)
|
||||||
|
for tag_name in JAM_HIDDEN_TAGS:
|
||||||
|
if tag_name not in vorhanden:
|
||||||
|
offset -= text
|
||||||
|
_jam_attdef(blk, tag_name, -w, offset, text)
|
||||||
return JAM_BLOCK
|
return JAM_BLOCK
|
||||||
blk = doc.blocks.new(name=JAM_BLOCK)
|
blk = doc.blocks.new(name=JAM_BLOCK)
|
||||||
_jam_form(blk, text)
|
_jam_form(blk, text)
|
||||||
w, h = text * JAM_HALB_BREITE, text * JAM_HALB_HOEHE
|
|
||||||
name = blk.add_attdef("NAME", insert=(0, 0), height=text * JAM_TEXT,
|
name = blk.add_attdef("NAME", insert=(0, 0), height=text * JAM_TEXT,
|
||||||
dxfattribs={"style": "Standard", "text": JAM_OFFEN})
|
dxfattribs={"style": "Standard", "text": JAM_OFFEN})
|
||||||
_jam_name(name, text)
|
_jam_name(name, text)
|
||||||
offset = -h - text
|
offset = -h - text
|
||||||
for tag_name in ("CONVEYOR", "NUMBER", "TRO", "ENTRY", "AREA", "AUTO"):
|
for tag_name in JAM_HIDDEN_TAGS:
|
||||||
blk.add_attdef(tag_name, insert=(-w, offset), height=text * 0.7,
|
_jam_attdef(blk, tag_name, -w, offset, text)
|
||||||
dxfattribs={"style": "Standard", "color": 256, "text": "",
|
|
||||||
"invisible": 1})
|
|
||||||
offset -= text
|
offset -= text
|
||||||
return JAM_BLOCK
|
return JAM_BLOCK
|
||||||
|
|
||||||
|
|
||||||
|
def _jam_attdef(blk, tag_name: str, x: float, y: float, text: float) -> None:
|
||||||
|
blk.add_attdef(tag_name, insert=(x, y), height=text * 0.7,
|
||||||
|
dxfattribs={"style": "Standard", "color": 256, "text": "",
|
||||||
|
"invisible": 1})
|
||||||
|
|
||||||
|
|
||||||
def _jam_form(blk, text: float) -> None:
|
def _jam_form(blk, text: float) -> None:
|
||||||
"""Stehendes Rechteck: graue Fuellung, grauer Rand."""
|
"""Stehendes Rechteck: graue Fuellung, grauer Rand."""
|
||||||
w, h = text * JAM_HALB_BREITE, text * JAM_HALB_HOEHE
|
w, h = text * JAM_HALB_BREITE, text * JAM_HALB_HOEHE
|
||||||
@@ -967,7 +992,7 @@ def place_jams(
|
|||||||
f"nicht gezeichnet")
|
f"nicht gezeichnet")
|
||||||
continue
|
continue
|
||||||
auto = automatisch.get(area.jam_id)
|
auto = automatisch.get(area.jam_id)
|
||||||
nr, k = overrides.get(jam_key(area), auto or ("", 0))
|
nr, k = jam_override(area, layout, overrides) or auto or ("", 0)
|
||||||
ref = msp.add_blockref(block, transform.to_dxf(*lage),
|
ref = msp.add_blockref(block, transform.to_dxf(*lage),
|
||||||
dxfattribs={"layer": layers["jam"]})
|
dxfattribs={"layer": layers["jam"]})
|
||||||
ref.add_auto_attribs({
|
ref.add_auto_attribs({
|
||||||
@@ -978,6 +1003,8 @@ def place_jams(
|
|||||||
"ENTRY": str(area.entry),
|
"ENTRY": str(area.entry),
|
||||||
"AREA": area.jam_id,
|
"AREA": area.jam_id,
|
||||||
"AUTO": f"{auto[0]}_{auto[1]}" if auto else "",
|
"AUTO": f"{auto[0]}_{auto[1]}" if auto else "",
|
||||||
|
# Schluessel fuer --read-jams: ueberlebt eine Umbenennung des TRO
|
||||||
|
"KEY": jam_stable_key(area, layout),
|
||||||
})
|
})
|
||||||
tag(ref, area.tro)
|
tag(ref, area.tro)
|
||||||
gezeichnet += 1
|
gezeichnet += 1
|
||||||
@@ -999,11 +1026,16 @@ def read_jams(dxf_file: Path, ini_file: Path) -> tuple[dict[str, tuple[str, int]
|
|||||||
namen: dict[str, str] = {}
|
namen: dict[str, str] = {}
|
||||||
for ref in doc.modelspace().query(f'INSERT[name=="{JAM_BLOCK}"]'):
|
for ref in doc.modelspace().query(f'INSERT[name=="{JAM_BLOCK}"]'):
|
||||||
a = {att.dxf.tag.upper(): (att.dxf.text or "").strip() for att in ref.attribs}
|
a = {att.dxf.tag.upper(): (att.dxf.text or "").strip() for att in ref.attribs}
|
||||||
key = f"{a.get('TRO', '')}.{a.get('ENTRY', '')}"
|
# Stabiler Schluessel (KEY, seit 2026-10-27), sonst TRO-ID + Eingang
|
||||||
|
anzeige = f"{a.get('TRO', '')}.{a.get('ENTRY', '')}"
|
||||||
|
key = a.get("KEY") or anzeige
|
||||||
nr, k = a.get("CONVEYOR", ""), a.get("NUMBER", "")
|
nr, k = a.get("CONVEYOR", ""), a.get("NUMBER", "")
|
||||||
if not a.get("TRO") or not a.get("ENTRY"):
|
if not a.get("TRO") or not a.get("ENTRY"):
|
||||||
meldungen.append(f"Staubereich ohne TRO/ENTRY ({a.get('NAME', '?')}) - uebersprungen")
|
meldungen.append(f"Staubereich ohne TRO/ENTRY ({a.get('NAME', '?')}) - uebersprungen")
|
||||||
continue
|
continue
|
||||||
|
if key != anzeige and anzeige in overrides:
|
||||||
|
# Eintrag unter der alten Anzeige-ID auf den Schluessel umstellen
|
||||||
|
overrides.setdefault(key, overrides.pop(anzeige))
|
||||||
if not nr and not k:
|
if not nr and not k:
|
||||||
continue
|
continue
|
||||||
if not (nr.isdigit() and len(nr) == 4 and k.isdigit() and int(k) > 0):
|
if not (nr.isdigit() and len(nr) == 4 and k.isdigit() and int(k) > 0):
|
||||||
@@ -1085,33 +1117,48 @@ def write_parts(doc, elements, transform: Transform) -> int:
|
|||||||
return len(tags) // 5
|
return len(tags) // 5
|
||||||
|
|
||||||
|
|
||||||
def read_markers(dxf_file: Path) -> list[dict[str, str]]:
|
def read_markers(dxf_file: Path) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
|
||||||
"""Attribute aller TRO-Marker (Bloecke TRO_SYM_*) der Zeichnung."""
|
"""Attribute aller TRO-Marker (Bloecke TRO_SYM_*) und aller
|
||||||
|
Verbindungsbloecke (CONNECTION_ARROW) der Zeichnung."""
|
||||||
doc = ezdxf.readfile(str(dxf_file))
|
doc = ezdxf.readfile(str(dxf_file))
|
||||||
markers = []
|
markers, arrows = [], []
|
||||||
for ref in doc.modelspace().query("INSERT"):
|
for ref in doc.modelspace().query("INSERT"):
|
||||||
if not ref.dxf.name.upper().startswith(BLOCK_PREFIX):
|
name = ref.dxf.name.upper()
|
||||||
|
if name.startswith(BLOCK_PREFIX):
|
||||||
|
ziel = markers
|
||||||
|
elif name == CONNECTION_BLOCK:
|
||||||
|
ziel = arrows
|
||||||
|
else:
|
||||||
continue
|
continue
|
||||||
a = {att.dxf.tag.upper(): (att.dxf.text or "").strip() for att in ref.attribs}
|
a = {att.dxf.tag.upper(): (att.dxf.text or "").strip() for att in ref.attribs}
|
||||||
a["_BLOCK"] = ref.dxf.name
|
a["_BLOCK"] = ref.dxf.name
|
||||||
markers.append(a)
|
ziel.append(a)
|
||||||
return markers
|
return markers, arrows
|
||||||
|
|
||||||
|
|
||||||
def read_tros(markers: list[dict[str, str]], raw: Analysis, elements,
|
def read_tros(markers: list[dict[str, str]], raw: Analysis, elements,
|
||||||
csv_name: str, cfg_dir: Path | None = None,
|
csv_name: str, cfg_dir: Path | None = None,
|
||||||
write: bool = True) -> tuple[list[TroEdit], list[str]]:
|
write: bool = True,
|
||||||
|
arrows: list[dict[str, str]] | None = None,
|
||||||
|
base: Analysis | None = None) -> tuple[list[TroEdit], list[str]]:
|
||||||
"""Marker der Zeichnung mit der Ableitung vergleichen -> Handkorrekturen.
|
"""Marker der Zeichnung mit der Ableitung vergleichen -> Handkorrekturen.
|
||||||
|
|
||||||
``raw`` ist die Ableitung OHNE Handkorrekturen (IDs aus der ID-Datei). Je
|
``raw`` ist die Ableitung OHNE Handkorrekturen (IDs aus der ID-Datei). Je
|
||||||
Marker mit KEY eines abgeleiteten TRO entsteht ein edit-Abschnitt, wenn
|
Marker mit KEY eines abgeleiteten TRO entsteht ein edit-Abschnitt, wenn
|
||||||
Typ, Kennung oder Bauteile abweichen; ein Marker ohne passenden KEY ist
|
Typ, Kennung oder Bauteile abweichen; ein Marker ohne passenden KEY ist
|
||||||
ein neuer TRO (edit_add); ein abgeleiteter TRO mit Lage, zu dem kein
|
ein neuer TRO (edit_add); ein abgeleiteter TRO mit Lage, zu dem kein
|
||||||
Marker mehr da ist, wurde geloescht. Die Abschnitte ersetzen alle
|
Marker mehr da ist, wurde geloescht. Mit ``arrows`` (Verbindungsbloecke)
|
||||||
bisherigen edit-/edit_add-Abschnitte der CSV in tro_overrides.ini
|
kommen die geaenderten Verbindungen dazu (_read_connections, Abschnitt
|
||||||
(Begruendungen bleiben). Rueckgabe: (Korrekturen, Meldungen).
|
edit_connections). Die Abschnitte ersetzen alle bisherigen
|
||||||
|
edit-/edit_add-/edit_connections-Abschnitte der CSV in tro_overrides.ini
|
||||||
|
(Begruendungen bleiben). Ohne ``arrows`` bleibt ein vorhandener
|
||||||
|
edit_connections-Abschnitt stehen. ``base`` ist die Ableitung MIT den
|
||||||
|
TRO-Korrekturen, aber ohne Verbindungskorrekturen - verglichen werden die
|
||||||
|
Pfeile mit ihr, damit Kanten eines geaenderten oder neuen TRO nicht als
|
||||||
|
Handkorrektur gelten (Standard: ``raw``). Rueckgabe: (Korrekturen, Meldungen).
|
||||||
"""
|
"""
|
||||||
meldungen: list[str] = []
|
meldungen: list[str] = []
|
||||||
|
marker_key: dict[int, str] = {}
|
||||||
by_id = {e.teile_id: e for e in elements}
|
by_id = {e.teile_id: e for e in elements}
|
||||||
alt = {(e.section_kind, e.key): e
|
alt = {(e.section_kind, e.key): e
|
||||||
for e in load_overrides(csv_name, cfg_dir).edits}
|
for e in load_overrides(csv_name, cfg_dir).edits}
|
||||||
@@ -1176,6 +1223,8 @@ def read_tros(markers: list[dict[str, str]], raw: Analysis, elements,
|
|||||||
raw_slots, raw_lines = tro_slots(derived)
|
raw_slots, raw_lines = tro_slots(derived)
|
||||||
else:
|
else:
|
||||||
raw_slots, raw_lines = {}, []
|
raw_slots, raw_lines = {}, []
|
||||||
|
# Schluessel des Markers fuer die Verbindungen: abgeleitet = KEY, neu = ID
|
||||||
|
marker_key[id(m)] = key if derived is not None else tro_id
|
||||||
if lines is None:
|
if lines is None:
|
||||||
lines = raw_lines if has_lines(type_name) else []
|
lines = raw_lines if has_lines(type_name) else []
|
||||||
pruefen(where, type_name, slots, lines)
|
pruefen(where, type_name, slots, lines)
|
||||||
@@ -1216,11 +1265,64 @@ def read_tros(markers: list[dict[str, str]], raw: Analysis, elements,
|
|||||||
edits.append(edit)
|
edits.append(edit)
|
||||||
meldungen.append(_describe_edit(edit))
|
meldungen.append(_describe_edit(edit))
|
||||||
|
|
||||||
|
if arrows is not None:
|
||||||
|
connections = _read_connections(arrows, markers, marker_key, base or raw, meldungen)
|
||||||
|
else:
|
||||||
|
# ohne Verbindungsbloecke: bisherige Verbindungskorrekturen behalten
|
||||||
|
connections = load_overrides(csv_name, cfg_dir).connection_edits
|
||||||
if write:
|
if write:
|
||||||
save_edits(csv_name, edits, cfg_dir)
|
save_edits(csv_name, edits, cfg_dir, connections)
|
||||||
return edits, meldungen
|
return edits, meldungen
|
||||||
|
|
||||||
|
|
||||||
|
def _read_connections(arrows: list[dict[str, str]], markers: list[dict[str, str]],
|
||||||
|
marker_key: dict[int, str], base: Analysis,
|
||||||
|
meldungen: list[str]) -> list[ConnectionEdit]:
|
||||||
|
"""Verbindungsbloecke der Zeichnung gegen die abgeleiteten TRO-Kanten.
|
||||||
|
|
||||||
|
Die Enden eines CONNECTION_ARROW werden ueber die Marker derselben
|
||||||
|
Zeichnung zu stabilen Schluesseln: erst ueber den sichtbaren Namen
|
||||||
|
(FROM_TRO/TO_TRO, das sieht der Bearbeiter), sonst ueber die interne
|
||||||
|
Nummer (FROM_UID/TO_UID, bei dxfmakros-Bloecken frueher FROM_ID/TO_ID) -
|
||||||
|
so stimmt es auch nach einer Umbenennung des Markers. Kanten von/zu
|
||||||
|
Stationen tragen keinen Block und bleiben, wie abgeleitet; Kanten zu
|
||||||
|
geloeschten Markern fallen mit dem TRO weg.
|
||||||
|
"""
|
||||||
|
by_name = {m.get("ID", ""): m for m in markers if id(m) in marker_key}
|
||||||
|
by_uid = {m.get("UID", ""): m for m in markers if id(m) in marker_key and m.get("UID")}
|
||||||
|
|
||||||
|
def ende(a: dict[str, str], seite: str) -> str | None:
|
||||||
|
m = by_name.get(a.get(f"{seite}_TRO", ""))
|
||||||
|
if m is None:
|
||||||
|
m = by_uid.get(a.get(f"{seite}_UID", "") or a.get(f"{seite}_ID", ""))
|
||||||
|
return marker_key.get(id(m)) if m is not None else None
|
||||||
|
|
||||||
|
gewollt: dict[tuple[str, str], str] = {}
|
||||||
|
for a in arrows:
|
||||||
|
src, dst = ende(a, "FROM"), ende(a, "TO")
|
||||||
|
if src is None or dst is None:
|
||||||
|
meldungen.append(f"Verbindung {a.get('FROM_TRO') or '?'} -> "
|
||||||
|
f"{a.get('TO_TRO') or '?'}: Ende passt zu keinem Marker - "
|
||||||
|
f"uebersprungen")
|
||||||
|
continue
|
||||||
|
if src == dst:
|
||||||
|
meldungen.append(f"Verbindung {src} -> {dst}: Quelle = Ziel - uebersprungen")
|
||||||
|
continue
|
||||||
|
gewollt[(src, dst)] = "weiche" if a.get("KIND") == "weiche" else ""
|
||||||
|
|
||||||
|
vorhanden = set(marker_key.values())
|
||||||
|
tro_ids = {t.tro_id for t in base.tros}
|
||||||
|
# Kanten der Ableitung, Enden als stabile Schluessel wie die Marker
|
||||||
|
abgeleitet = {(base.edit_key(s), base.edit_key(d)) for s, d in base.edges
|
||||||
|
if s in tro_ids and d in tro_ids}
|
||||||
|
changes = [ConnectionEdit(add=False, src=s, dst=d) for s, d in sorted(abgeleitet)
|
||||||
|
if (s, d) not in gewollt and s in vorhanden and d in vorhanden]
|
||||||
|
changes += [ConnectionEdit(add=True, src=s, dst=d, kind=k)
|
||||||
|
for (s, d), k in sorted(gewollt.items()) if (s, d) not in abgeleitet]
|
||||||
|
meldungen += [f"Verbindung {c.render()}" for c in changes]
|
||||||
|
return changes
|
||||||
|
|
||||||
|
|
||||||
def _describe_edit(edit: TroEdit) -> str:
|
def _describe_edit(edit: TroEdit) -> str:
|
||||||
if edit.delete:
|
if edit.delete:
|
||||||
return f"{edit.key}: geloescht"
|
return f"{edit.key}: geloescht"
|
||||||
@@ -1462,7 +1564,27 @@ def _main_read_jams(args, csv_file: Path) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _derive(args, csv_file: Path, edits: list[TroEdit] | None):
|
def analysis_from_document(doc: TroDocument) -> Analysis:
|
||||||
|
"""Analysis aus einer TRO-/Anlagen-JSON, damit die Zeichenfunktionen
|
||||||
|
(place_markers, draw_connections, place_stations) die JSON zeigen statt
|
||||||
|
einer neuen Ableitung. Umbenennungen kommen aus DerivedTro.key."""
|
||||||
|
tros = [t.model_copy(deep=True) for t in doc.tros]
|
||||||
|
analysis = Analysis(
|
||||||
|
tros=tros,
|
||||||
|
boundaries=[b.model_copy(deep=True) for b in doc.boundaries],
|
||||||
|
edges=[(c.src, c.dst) for c in doc.connections],
|
||||||
|
edge_kinds={(c.src, c.dst): c.kind for c in doc.connections},
|
||||||
|
findings=list(doc.findings),
|
||||||
|
renamed={t.tro_id: t.key for t in tros if t.key and t.key != t.tro_id},
|
||||||
|
method=f"Anlagen-/TRO-JSON {doc.source}",
|
||||||
|
)
|
||||||
|
analysis.unknown_types = sorted({t.type_name for t in tros if t.definition() is None})
|
||||||
|
analysis.isolated = [t for t in tros if not t.connected]
|
||||||
|
return analysis
|
||||||
|
|
||||||
|
|
||||||
|
def _derive(args, csv_file: Path, edits: list[TroEdit] | None,
|
||||||
|
connection_edits: list[ConnectionEdit] | None = None):
|
||||||
"""CSV lesen und die TROs ableiten - (Elemente, Analysis) oder None.
|
"""CSV lesen und die TROs ableiten - (Elemente, Analysis) oder None.
|
||||||
|
|
||||||
Derselbe Aufruf wie in tro_flow.main (mit der Separator-Gegenprobe aus
|
Derselbe Aufruf wie in tro_flow.main (mit der Separator-Gegenprobe aus
|
||||||
@@ -1490,7 +1612,8 @@ def _derive(args, csv_file: Path, edits: list[TroEdit] | None):
|
|||||||
print(f"FEHLER: ID-Datei nicht lesbar: {exc}", file=sys.stderr)
|
print(f"FEHLER: ID-Datei nicht lesbar: {exc}", file=sys.stderr)
|
||||||
return None
|
return None
|
||||||
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts,
|
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts,
|
||||||
tro_ids=tro_ids, edits=edits)
|
tro_ids=tro_ids, edits=edits,
|
||||||
|
connection_edits=connection_edits)
|
||||||
if not analysis.tros:
|
if not analysis.tros:
|
||||||
print(f"FEHLER: keine TROs aus {csv_file} ableitbar.", file=sys.stderr)
|
print(f"FEHLER: keine TROs aus {csv_file} ableitbar.", file=sys.stderr)
|
||||||
return None
|
return None
|
||||||
@@ -1514,7 +1637,7 @@ def _main_read_tros(args, csv_file: Path) -> int:
|
|||||||
print(f"FEHLER: CSV nicht gefunden: {csv_file}", file=sys.stderr)
|
print(f"FEHLER: CSV nicht gefunden: {csv_file}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
try:
|
try:
|
||||||
markers = read_markers(dxf_file)
|
markers, arrows = read_markers(dxf_file)
|
||||||
except (OSError, ezdxf.DXFError) as exc:
|
except (OSError, ezdxf.DXFError) as exc:
|
||||||
print(f"FEHLER: {dxf_file} nicht lesbar: {exc}", file=sys.stderr)
|
print(f"FEHLER: {dxf_file} nicht lesbar: {exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
@@ -1531,8 +1654,15 @@ def _main_read_tros(args, csv_file: Path) -> int:
|
|||||||
f"mit dem aktuellen tro_annotate neu beschriften?", file=sys.stderr)
|
f"mit dem aktuellen tro_annotate neu beschriften?", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
try:
|
try:
|
||||||
|
# 1. TRO-Korrekturen aus den Markern; 2. die Pfeile gegen die Ableitung
|
||||||
|
# MIT diesen Korrekturen (sonst gaelten Kanten eines neuen TRO als
|
||||||
|
# von Hand gezogen)
|
||||||
|
edits, _ = read_tros(markers, raw, elements, csv_file.name, cfg_dir, write=False)
|
||||||
|
derived = _derive(args, csv_file, edits)
|
||||||
|
if derived is None:
|
||||||
|
return 1
|
||||||
edits, meldungen = read_tros(markers, raw, elements, csv_file.name, cfg_dir,
|
edits, meldungen = read_tros(markers, raw, elements, csv_file.name, cfg_dir,
|
||||||
write=not args.check)
|
write=not args.check, arrows=arrows, base=derived[1])
|
||||||
except (OSError, configparser.Error) as exc:
|
except (OSError, configparser.Error) as exc:
|
||||||
print(f"FEHLER: {exc}", file=sys.stderr)
|
print(f"FEHLER: {exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
@@ -1540,7 +1670,7 @@ def _main_read_tros(args, csv_file: Path) -> int:
|
|||||||
print("================================================================")
|
print("================================================================")
|
||||||
print("TRO-HANDKORREKTUREN UEBERNEHMEN")
|
print("TRO-HANDKORREKTUREN UEBERNEHMEN")
|
||||||
print("================================================================")
|
print("================================================================")
|
||||||
print(f"Zeichnung = {dxf_file} ({len(markers)} Marker)")
|
print(f"Zeichnung = {dxf_file} ({len(markers)} Marker, {len(arrows)} Verbindungen)")
|
||||||
ziel = override_path(cfg_dir)
|
ziel = override_path(cfg_dir)
|
||||||
print(f"Korrekturen = {len(edits)} -> "
|
print(f"Korrekturen = {len(edits)} -> "
|
||||||
f"{'- (--check, nichts geschrieben)' if args.check else ziel}")
|
f"{'- (--check, nichts geschrieben)' if args.check else ziel}")
|
||||||
@@ -1624,7 +1754,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
# TROs aus dem CSV, mit den Handkorrekturen aus tro_overrides.ini
|
# TROs aus dem CSV, mit den Handkorrekturen aus tro_overrides.ini
|
||||||
overrides = load_overrides(csv_file.name, cfg_dir)
|
overrides = load_overrides(csv_file.name, cfg_dir)
|
||||||
warnings += [w for w in overrides.warnings if ".edit" in w]
|
warnings += [w for w in overrides.warnings if ".edit" in w]
|
||||||
derived = _derive(args, csv_file, overrides.edits)
|
derived = _derive(args, csv_file, overrides.edits, overrides.connection_edits)
|
||||||
if derived is None:
|
if derived is None:
|
||||||
return 1
|
return 1
|
||||||
elements, analysis = derived
|
elements, analysis = derived
|
||||||
@@ -1654,14 +1784,17 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
counts = {"marker": 0, "flow": 0, "removed": 0, "layers": 0, "blocks": 0,
|
counts = {"marker": 0, "flow": 0, "removed": 0, "layers": 0, "blocks": 0,
|
||||||
"first_id": 0, "jams": 0}
|
"first_id": 0, "jams": 0}
|
||||||
if anlage is not None:
|
if anlage is not None:
|
||||||
|
# Mit Anlagen-JSON zeigt die Zeichnung genau sie - das, woraus die SCL
|
||||||
|
# wird. Die Ableitung aus dem CSV dient nur noch als Gegenprobe.
|
||||||
aus_csv = {t.tro_id for t in analysis.tros}
|
aus_csv = {t.tro_id for t in analysis.tros}
|
||||||
aus_json = {t.tro_id for t in anlage.layout.tros}
|
aus_json = {t.tro_id for t in anlage.layout.tros}
|
||||||
if aus_csv != aus_json:
|
if aus_csv != aus_json:
|
||||||
warnings.append(
|
warnings.append(
|
||||||
f"TROs der Zeichnung und der Anlagen-JSON weichen ab (nur CSV: "
|
f"Die Zeichnung zeigt die Anlagen-JSON; die Ableitung aus dem CSV weicht "
|
||||||
f"{', '.join(sorted(aus_csv - aus_json)) or '-'}; nur JSON: "
|
f"ab (nur CSV: {', '.join(sorted(aus_csv - aus_json)) or '-'}; nur JSON: "
|
||||||
f"{', '.join(sorted(aus_json - aus_csv)) or '-'}) - tro_flow --json und "
|
f"{', '.join(sorted(aus_json - aus_csv)) or '-'}) - JSON veraltet? "
|
||||||
f"merge_hw_layout neu laufen lassen")
|
f"tro_flow --json und merge_hw_layout neu laufen lassen")
|
||||||
|
analysis = analysis_from_document(anlage.layout)
|
||||||
|
|
||||||
if args.check:
|
if args.check:
|
||||||
counts["marker"] = sum(1 for t in analysis.tros if t.position)
|
counts["marker"] = sum(1 for t in analysis.tros if t.position)
|
||||||
|
|||||||
+49
-2
@@ -217,7 +217,7 @@ from tro_catalog import (
|
|||||||
get_tro,
|
get_tro,
|
||||||
mermaid_classdefs,
|
mermaid_classdefs,
|
||||||
)
|
)
|
||||||
from tro_overrides import PartRef, TroEdit, load_overrides
|
from tro_overrides import ConnectionEdit, PartRef, TroEdit, load_overrides
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Konstanten
|
# Konstanten
|
||||||
@@ -2428,6 +2428,47 @@ def apply_edits(tros: list[DerivedTro], edits: list[TroEdit], elements: list[Ele
|
|||||||
return tros
|
return tros
|
||||||
|
|
||||||
|
|
||||||
|
def apply_connection_edits(analysis: Analysis, changes: list[ConnectionEdit]) -> None:
|
||||||
|
"""Im Plan geaenderte Verbindungen (tro_overrides.ini, edit_connections).
|
||||||
|
|
||||||
|
Laeuft nach link_tros: '-' nimmt eine abgeleitete Kante weg, '+' fuegt eine
|
||||||
|
hinzu (Art aus dem Eintrag, sonst transfer). Die Enden sind stabile
|
||||||
|
Schluessel; ueber Analysis.renamed werden sie zur aktuellen ID. Danach
|
||||||
|
werden Vorgaenger/Nachfolger neu gesetzt - die Staubereiche folgen daraus.
|
||||||
|
"""
|
||||||
|
tros = analysis.tros + analysis.boundaries
|
||||||
|
id_of = {analysis.edit_key(t.tro_id): t.tro_id for t in tros}
|
||||||
|
id_of.update({t.tro_id: t.tro_id for t in tros})
|
||||||
|
edges = set(analysis.edges)
|
||||||
|
for change in changes:
|
||||||
|
src, dst = id_of.get(change.src), id_of.get(change.dst)
|
||||||
|
if src is None or dst is None:
|
||||||
|
fehlt = change.src if src is None else change.dst
|
||||||
|
analysis.findings.append(f"Handkorrektur Verbindung {change.render()}: {fehlt} "
|
||||||
|
f"gibt es nicht - ignoriert")
|
||||||
|
continue
|
||||||
|
if change.add:
|
||||||
|
if (src, dst) not in edges:
|
||||||
|
edges.add((src, dst))
|
||||||
|
analysis.findings.append(f"Handkorrektur Verbindung: {src} -> {dst} von Hand "
|
||||||
|
f"hinzugefuegt")
|
||||||
|
analysis.edge_kinds[(src, dst)] = change.kind or "transfer"
|
||||||
|
elif change.kind:
|
||||||
|
analysis.edge_kinds[(src, dst)] = change.kind
|
||||||
|
elif (src, dst) in edges:
|
||||||
|
edges.discard((src, dst))
|
||||||
|
analysis.edge_kinds.pop((src, dst), None)
|
||||||
|
analysis.findings.append(f"Handkorrektur Verbindung: {src} -> {dst} von Hand "
|
||||||
|
f"entfernt")
|
||||||
|
else:
|
||||||
|
analysis.findings.append(f"Handkorrektur Verbindung {change.render()}: wird "
|
||||||
|
f"nicht abgeleitet - nichts zu entfernen")
|
||||||
|
analysis.edges = sorted(edges)
|
||||||
|
for tro in tros:
|
||||||
|
tro.successors = sorted(d for s, d in analysis.edges if s == tro.tro_id)
|
||||||
|
tro.predecessors = sorted(s for s, d in analysis.edges if d == tro.tro_id)
|
||||||
|
|
||||||
|
|
||||||
def analyse_tros(
|
def analyse_tros(
|
||||||
graph: Graph,
|
graph: Graph,
|
||||||
elements: list[Element],
|
elements: list[Element],
|
||||||
@@ -2435,6 +2476,7 @@ def analyse_tros(
|
|||||||
sepliste_counts: dict[str, int] | None = None,
|
sepliste_counts: dict[str, int] | None = None,
|
||||||
tro_ids: TroIds | None = None,
|
tro_ids: TroIds | None = None,
|
||||||
edits: list[TroEdit] | None = None,
|
edits: list[TroEdit] | None = None,
|
||||||
|
connection_edits: list[ConnectionEdit] | None = None,
|
||||||
) -> Analysis:
|
) -> Analysis:
|
||||||
"""TROs ermitteln, verknuepfen, verorten und gegen die Typdefinition pruefen.
|
"""TROs ermitteln, verknuepfen, verorten und gegen die Typdefinition pruefen.
|
||||||
|
|
||||||
@@ -2452,6 +2494,8 @@ def analyse_tros(
|
|||||||
nummerieren.
|
nummerieren.
|
||||||
edits: Handkorrekturen aus BricsCAD (tro_overrides.load_overrides
|
edits: Handkorrekturen aus BricsCAD (tro_overrides.load_overrides
|
||||||
(...).edits), angewandt direkt nach derive_tros (apply_edits).
|
(...).edits), angewandt direkt nach derive_tros (apply_edits).
|
||||||
|
connection_edits: im Plan geaenderte Verbindungen
|
||||||
|
(load_overrides(...).connection_edits), angewandt nach link_tros.
|
||||||
|
|
||||||
Rueckgabe:
|
Rueckgabe:
|
||||||
Analysis mit tros, boundaries, edges/edge_kinds, unknown_types
|
Analysis mit tros, boundaries, edges/edge_kinds, unknown_types
|
||||||
@@ -2476,6 +2520,8 @@ def analyse_tros(
|
|||||||
# Kreisel-Bahn wird ueber ihre Koordinate bestimmt.
|
# Kreisel-Bahn wird ueber ihre Koordinate bestimmt.
|
||||||
locate_tros(linked, elements, graph)
|
locate_tros(linked, elements, graph)
|
||||||
analysis.edges, analysis.edge_kinds = link_tros(graph, linked, elements)
|
analysis.edges, analysis.edge_kinds = link_tros(graph, linked, elements)
|
||||||
|
if connection_edits:
|
||||||
|
apply_connection_edits(analysis, connection_edits)
|
||||||
|
|
||||||
analysis.unknown_types = sorted(
|
analysis.unknown_types = sorted(
|
||||||
{tro.type_name for tro in analysis.tros if tro.definition() is None})
|
{tro.type_name for tro in analysis.tros if tro.definition() is None})
|
||||||
@@ -3598,7 +3644,8 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
warnings.append(f"--renumber: {len(overrides.edits)} Handkorrektur(en) in "
|
warnings.append(f"--renumber: {len(overrides.edits)} Handkorrektur(en) in "
|
||||||
f"{overrides.source} haengen an den alten IDs - pruefen")
|
f"{overrides.source} haengen an den alten IDs - pruefen")
|
||||||
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts,
|
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts,
|
||||||
tro_ids=tro_ids, edits=overrides.edits)
|
tro_ids=tro_ids, edits=overrides.edits,
|
||||||
|
connection_edits=overrides.connection_edits)
|
||||||
if not analysis.tros:
|
if not analysis.tros:
|
||||||
print(f"FEHLER: {csv_file} enthaelt keine Separatoren - keine TROs "
|
print(f"FEHLER: {csv_file} enthaelt keine Separatoren - keine TROs "
|
||||||
f"ableitbar.", file=sys.stderr)
|
f"ableitbar.", file=sys.stderr)
|
||||||
|
|||||||
+55
-4
@@ -56,6 +56,11 @@ anders als die laufenden Nummern, an denen die alten Abschnitte veraltet sind.
|
|||||||
|
|
||||||
[<csv>.edit_add.<ID>] neuer TRO (TRO_INSERT), Schluessel wie oben
|
[<csv>.edit_add.<ID>] neuer TRO (TRO_INSERT), Schluessel wie oben
|
||||||
|
|
||||||
|
[<csv>.edit_connections] Verbindungen, im Plan (CONNECTION_*) geaendert
|
||||||
|
c01 = + TRO05 -> TRO07 | transfer dazu (Art optional, Standard transfer)
|
||||||
|
c02 = - TRO05 -> TRO01 weg
|
||||||
|
Enden sind die stabilen Schluessel der TROs
|
||||||
|
|
||||||
Stehen Slots im Abschnitt, gelten sie vollstaendig (fehlende Slots des Typs
|
Stehen Slots im Abschnitt, gelten sie vollstaendig (fehlende Slots des Typs
|
||||||
bleiben leer); stehen keine, bleiben die Bauteile wie abgeleitet. Die Lage
|
bleiben leer); stehen keine, bleiben die Bauteile wie abgeleitet. Die Lage
|
||||||
hinter der TeileId macht den Eintrag exportfest: passt die TeileId im neuen
|
hinter der TeileId macht den Eintrag exportfest: passt die TeileId im neuen
|
||||||
@@ -220,6 +225,33 @@ class TroEdit:
|
|||||||
or self.slots is not None or self.lines is not None)
|
or self.slots is not None or self.lines is not None)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ConnectionEdit:
|
||||||
|
"""Eine im Plan hinzugefuegte (add) oder entfernte Verbindung zwischen
|
||||||
|
zwei TROs, Enden als stabile Schluessel (DerivedTro.key)."""
|
||||||
|
|
||||||
|
add: bool
|
||||||
|
src: str
|
||||||
|
dst: str
|
||||||
|
kind: str = ""
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
text = f"{'+' if self.add else '-'} {self.src} -> {self.dst}"
|
||||||
|
return f"{text} | {self.kind}" if self.add and self.kind else text
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, raw: str) -> ConnectionEdit:
|
||||||
|
text = raw.strip()
|
||||||
|
if not text or text[0] not in "+-":
|
||||||
|
raise ValueError(f"'{raw}': erwartet '+ <von> -> <nach> [| Art]' oder "
|
||||||
|
f"'- <von> -> <nach>'")
|
||||||
|
pfeil, _, kind = text[1:].partition("|")
|
||||||
|
src, sep, dst = (t.strip() for t in pfeil.partition("->"))
|
||||||
|
if not sep or not src or not dst:
|
||||||
|
raise ValueError(f"'{raw}': erwartet '<von> -> <nach>'")
|
||||||
|
return cls(add=text[0] == "+", src=src, dst=dst, kind=kind.strip())
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Overrides:
|
class Overrides:
|
||||||
"""Alle Korrekturen zu einer CSV-Datei.
|
"""Alle Korrekturen zu einer CSV-Datei.
|
||||||
@@ -237,6 +269,8 @@ class Overrides:
|
|||||||
candidates: list[TroCandidate] = field(default_factory=list)
|
candidates: list[TroCandidate] = field(default_factory=list)
|
||||||
# Handkorrekturen aus BricsCAD in Dateireihenfolge (tro_flow.apply_edits)
|
# Handkorrekturen aus BricsCAD in Dateireihenfolge (tro_flow.apply_edits)
|
||||||
edits: list[TroEdit] = field(default_factory=list)
|
edits: list[TroEdit] = field(default_factory=list)
|
||||||
|
# Verbindungen aus BricsCAD (tro_flow.apply_connection_edits)
|
||||||
|
connection_edits: list[ConnectionEdit] = field(default_factory=list)
|
||||||
# Protokoll der tatsaechlich angewandten Korrekturen (fuer die Doku)
|
# Protokoll der tatsaechlich angewandten Korrekturen (fuer die Doku)
|
||||||
applied: list[str] = field(default_factory=list)
|
applied: list[str] = field(default_factory=list)
|
||||||
warnings: list[str] = field(default_factory=list)
|
warnings: list[str] = field(default_factory=list)
|
||||||
@@ -278,7 +312,8 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
|||||||
|
|
||||||
parser = configparser.ConfigParser(inline_comment_prefixes=(";",))
|
parser = configparser.ConfigParser(inline_comment_prefixes=(";",))
|
||||||
# Schluessel sind Kleinschreibung, Sektionsnamen bleiben wie geschrieben.
|
# Schluessel sind Kleinschreibung, Sektionsnamen bleiben wie geschrieben.
|
||||||
parser.read(path, encoding="utf-8")
|
# utf-8-sig: von Hand (Editor, PowerShell) gespeichert oft mit BOM
|
||||||
|
parser.read(path, encoding="utf-8-sig")
|
||||||
|
|
||||||
prefix = csv_name.lower()
|
prefix = csv_name.lower()
|
||||||
for section in parser.sections():
|
for section in parser.sections():
|
||||||
@@ -297,6 +332,14 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
|||||||
Connection(src=src, dst=dst, kind=rest.strip())
|
Connection(src=src, dst=dst, kind=rest.strip())
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
# [<csv>.edit_connections] - Verbindungen aus BricsCAD
|
||||||
|
if section.lower() == f"{prefix}.edit_connections":
|
||||||
|
for key, raw in parser[section].items():
|
||||||
|
try:
|
||||||
|
result.connection_edits.append(ConnectionEdit.parse(raw))
|
||||||
|
except ValueError as exc:
|
||||||
|
result.warnings.append(f"[{section}] {key}: {exc}")
|
||||||
|
continue
|
||||||
|
|
||||||
parts = section.split(".")
|
parts = section.split(".")
|
||||||
# <csv>.<art>.<name> - der CSV-Name kann selbst einen Punkt enthalten
|
# <csv>.<art>.<name> - der CSV-Name kann selbst einen Punkt enthalten
|
||||||
@@ -475,8 +518,10 @@ def render_edit(csv_name: str, edit: TroEdit) -> str:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def save_edits(csv_name: str, edits: list[TroEdit], cfg_dir: Path | None = None) -> Path:
|
def save_edits(csv_name: str, edits: list[TroEdit], cfg_dir: Path | None = None,
|
||||||
"""Alle edit-/edit_add-Abschnitte zu `csv_name` durch `edits` ersetzen.
|
connections: list[ConnectionEdit] | None = None) -> Path:
|
||||||
|
"""Alle edit-/edit_add-Abschnitte und [<csv>.edit_connections] zu
|
||||||
|
`csv_name` durch `edits` bzw. `connections` ersetzen.
|
||||||
|
|
||||||
Der Rest der Datei (von Hand gepflegt, mit Kommentaren) bleibt Zeile fuer
|
Der Rest der Datei (von Hand gepflegt, mit Kommentaren) bleibt Zeile fuer
|
||||||
Zeile erhalten; der neue Block steht am Ende. Kommentarzeilen direkt vor
|
Zeile erhalten; der neue Block steht am Ende. Kommentarzeilen direkt vor
|
||||||
@@ -488,6 +533,8 @@ def save_edits(csv_name: str, edits: list[TroEdit], cfg_dir: Path | None = None)
|
|||||||
head = _EDIT_HEAD.format(csv=csv_name).lower()
|
head = _EDIT_HEAD.format(csv=csv_name).lower()
|
||||||
|
|
||||||
def ist_edit(section: str) -> bool:
|
def ist_edit(section: str) -> bool:
|
||||||
|
if section.lower() == f"{prefix}.edit_connections":
|
||||||
|
return True
|
||||||
parts = section.split(".")
|
parts = section.split(".")
|
||||||
return (len(parts) >= 3 and parts[-2].lower() in ("edit", "edit_add")
|
return (len(parts) >= 3 and parts[-2].lower() in ("edit", "edit_add")
|
||||||
and ".".join(parts[:-2]).lower() == prefix)
|
and ".".join(parts[:-2]).lower() == prefix)
|
||||||
@@ -519,13 +566,17 @@ def save_edits(csv_name: str, edits: list[TroEdit], cfg_dir: Path | None = None)
|
|||||||
|
|
||||||
while keep and not keep[-1].strip():
|
while keep and not keep[-1].strip():
|
||||||
keep.pop()
|
keep.pop()
|
||||||
if edits:
|
if edits or connections:
|
||||||
if keep:
|
if keep:
|
||||||
keep += ["", ""]
|
keep += ["", ""]
|
||||||
keep.append(_EDIT_HEAD.format(csv=csv_name)
|
keep.append(_EDIT_HEAD.format(csv=csv_name)
|
||||||
+ f" - tro_annotate --read-tros, {datetime.now():%Y-%m-%d}")
|
+ f" - tro_annotate --read-tros, {datetime.now():%Y-%m-%d}")
|
||||||
for edit in edits:
|
for edit in edits:
|
||||||
keep += [render_edit(csv_name, edit), ""]
|
keep += [render_edit(csv_name, edit), ""]
|
||||||
|
if connections:
|
||||||
|
keep.append(f"[{csv_name}.edit_connections]")
|
||||||
|
keep += [f"c{n:02d} = {c.render()}" for n, c in enumerate(connections, start=1)]
|
||||||
|
keep.append("")
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_text("\n".join(keep).rstrip("\n") + "\n", encoding="utf-8")
|
path.write_text("\n".join(keep).rstrip("\n") + "\n", encoding="utf-8")
|
||||||
return path
|
return path
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Absichtlich ohne Shebang: der Windows-py-Launcher wuerde daraus "python3"
|
||||||
|
# ableiten und scheitert, wenn keine PythonCore-Installation registriert ist.
|
||||||
|
"""
|
||||||
|
validate_anlage.py - Ist die Anlagen-JSON bereit fuer die SCL?
|
||||||
|
|
||||||
|
Eine Pruefung fuer beide Wege des Ablaufs (doc/Konzept_Layout_Hardware_SCL.md):
|
||||||
|
direkt nach merge_hw_layout (keine Aenderungen) und nach den Handkorrekturen
|
||||||
|
in BricsCAD (tro_annotate --read-tros/--read-jams, dann tro_flow --json und
|
||||||
|
merge_hw_layout neu). scl_skeleton ruft sie vor dem Schreiben auf.
|
||||||
|
|
||||||
|
Geprueft wird (FEHLER = so keine SCL, WARNUNG = SCL mit offenen Stellen):
|
||||||
|
|
||||||
|
Struktur TRO-IDs eindeutig, jeder Typ im Katalog, Verbindungen und
|
||||||
|
Staubereiche zeigen auf vorhandene TROs/Stationen, kein
|
||||||
|
Separator in zwei TROs (FEHLER); leerer Separator-Slot, TRO
|
||||||
|
ohne Verbindung, TRO vom Materialeingang aus nicht erreichbar
|
||||||
|
(WARNUNG)
|
||||||
|
Zuordnungen Binding auf einen unbekannten TRO/Slot, Status widerspruch,
|
||||||
|
BMK fehlt in der Hardwareplanung (FEHLER); offene
|
||||||
|
Pflichtrollen (WARNUNG, je Rolle gezaehlt)
|
||||||
|
Aktualitaet Anlagen-JSON aelter als TRO-JSON, Hardwareplanung oder
|
||||||
|
hw_overrides.ini (FEHLER: merge_hw_layout neu);
|
||||||
|
TRO-JSON aelter als die BricsCAD-Korrekturen in
|
||||||
|
tro_overrides.ini (WARNUNG: tro_flow --json neu)
|
||||||
|
Korrekturen Eintraege in hw_overrides.ini / <csv>_jam_names.ini, die zu
|
||||||
|
keinem TRO bzw. Staubereich passen - weder ueber die ID noch
|
||||||
|
ueber den stabilen Schluessel (WARNUNG)
|
||||||
|
|
||||||
|
Aufruf ueber bin/validate_anlage.bat bzw. bin/validate_anlage.sh:
|
||||||
|
|
||||||
|
validate_anlage --json mubea_anlage.json [--hw mubea_hw.json] [--strict]
|
||||||
|
|
||||||
|
Exit-Codes: 0 bereit (mit --strict: auch ohne Warnungen), 1 Eingabefehler,
|
||||||
|
5 Fehler gefunden (mit --strict auch bei Warnungen).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import collections
|
||||||
|
import configparser
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from hw_import import HardwareDocument
|
||||||
|
from material_flow import env_dir
|
||||||
|
from model import ANLAGE_SCHEMA, AnlageDocument, TroDocument
|
||||||
|
from scl_context import JAM_NAMES_SUFFIX, jam_key, jam_stable_key, load_jam_overrides
|
||||||
|
from tro_overrides import load_overrides, override_path
|
||||||
|
|
||||||
|
FEHLER = "FEHLER"
|
||||||
|
WARNUNG = "WARNUNG"
|
||||||
|
# Name der Datei mit den Handkorrekturen der Hardware (merge_hw_layout)
|
||||||
|
HW_OVERRIDES_FILE = "hw_overrides.ini"
|
||||||
|
EXIT_FEHLER = 5
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Befund:
|
||||||
|
stufe: str
|
||||||
|
bereich: str
|
||||||
|
text: str
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.stufe:<7} {self.bereich:<11} {self.text}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Pruefung:
|
||||||
|
"""Ergebnis einer Pruefung, Befunde in der Reihenfolge der Bereiche."""
|
||||||
|
|
||||||
|
befunde: list[Befund] = field(default_factory=list)
|
||||||
|
|
||||||
|
def fehler(self, bereich: str, text: str) -> None:
|
||||||
|
self.befunde.append(Befund(FEHLER, bereich, text))
|
||||||
|
|
||||||
|
def warnung(self, bereich: str, text: str) -> None:
|
||||||
|
self.befunde.append(Befund(WARNUNG, bereich, text))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def anzahl_fehler(self) -> int:
|
||||||
|
return sum(1 for b in self.befunde if b.stufe == FEHLER)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def anzahl_warnungen(self) -> int:
|
||||||
|
return sum(1 for b in self.befunde if b.stufe == WARNUNG)
|
||||||
|
|
||||||
|
def bereit(self, strict: bool = False) -> bool:
|
||||||
|
return not self.anzahl_fehler and not (strict and self.anzahl_warnungen)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Einzelpruefungen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def check_structure(doc: TroDocument, p: Pruefung) -> None:
|
||||||
|
"""IDs, Typen, Verbindungen, Staubereiche, Separatoren, Erreichbarkeit."""
|
||||||
|
ids = collections.Counter(t.tro_id for t in doc.tros + doc.boundaries)
|
||||||
|
for tro_id, n in sorted(ids.items()):
|
||||||
|
if n > 1:
|
||||||
|
p.fehler("Struktur", f"TRO-ID {tro_id} kommt {n}x vor")
|
||||||
|
for tro in doc.tros:
|
||||||
|
if tro.definition() is None:
|
||||||
|
p.fehler("Struktur", f"{tro.tro_id}: Typ '{tro.type_name}' steht nicht im Katalog")
|
||||||
|
bekannt = set(ids)
|
||||||
|
for c in doc.connections:
|
||||||
|
for ende in (c.src, c.dst):
|
||||||
|
if ende not in bekannt:
|
||||||
|
p.fehler("Struktur", f"Verbindung {c.src} -> {c.dst}: {ende} unbekannt")
|
||||||
|
for area in doc.jam_areas_or_derived():
|
||||||
|
if area.tro not in bekannt:
|
||||||
|
p.fehler("Struktur", f"Staubereich {area.jam_id}: TRO {area.tro} unbekannt")
|
||||||
|
|
||||||
|
besitzer: dict[str, str] = {}
|
||||||
|
for tro in doc.tros:
|
||||||
|
for sep in dict.fromkeys(tro.separators):
|
||||||
|
if not sep:
|
||||||
|
continue
|
||||||
|
if sep in besitzer:
|
||||||
|
p.fehler("Struktur", f"Separator {sep} steckt in {besitzer[sep]} und "
|
||||||
|
f"{tro.tro_id}")
|
||||||
|
besitzer.setdefault(sep, tro.tro_id)
|
||||||
|
for slot in tro.slots:
|
||||||
|
if slot.slot.startswith("separator") and not slot.layout_id:
|
||||||
|
p.warnung("Struktur", f"{tro.tro_id}: {slot.slot} ohne Layout-Bauteil")
|
||||||
|
|
||||||
|
# Erreichbarkeit vom Materialeingang (Systemgrenzen ohne Vorgaenger)
|
||||||
|
nach: dict[str, set[str]] = collections.defaultdict(set)
|
||||||
|
for c in doc.connections:
|
||||||
|
nach[c.src].add(c.dst)
|
||||||
|
for tro in doc.tros + doc.boundaries:
|
||||||
|
nach[tro.tro_id].update(tro.successors)
|
||||||
|
for tro in doc.tros:
|
||||||
|
if not (tro.successors or tro.predecessors or tro.colocated):
|
||||||
|
p.warnung("Struktur", f"{tro.tro_id}: ohne Verbindung")
|
||||||
|
starts = [b.tro_id for b in doc.boundaries if not b.predecessors]
|
||||||
|
if starts:
|
||||||
|
gesehen, stapel = set(starts), list(starts)
|
||||||
|
while stapel:
|
||||||
|
for nxt in nach.get(stapel.pop(), ()):
|
||||||
|
if nxt not in gesehen:
|
||||||
|
gesehen.add(nxt)
|
||||||
|
stapel.append(nxt)
|
||||||
|
weg = [t.tro_id for t in doc.tros if t.tro_id not in gesehen
|
||||||
|
and (t.successors or t.predecessors)]
|
||||||
|
if weg:
|
||||||
|
p.warnung("Struktur", f"vom Materialeingang nicht erreichbar: {', '.join(weg)}")
|
||||||
|
|
||||||
|
|
||||||
|
def check_bindings(anlage: AnlageDocument, hw: HardwareDocument | None,
|
||||||
|
p: Pruefung) -> None:
|
||||||
|
"""Zuordnungen der Hardwareplanung gegen Layout und TIA-Import."""
|
||||||
|
doc = anlage.layout
|
||||||
|
slots = {(t.tro_id, s.slot) for t in doc.tros for s in t.slots}
|
||||||
|
tros = {t.tro_id for t in doc.tros}
|
||||||
|
geraete = hw.by_bmk() if hw is not None else None
|
||||||
|
offen: collections.Counter[str] = collections.Counter()
|
||||||
|
for b in anlage.bindings:
|
||||||
|
wo = f"{b.tro}.{b.slot}.{b.role}" if b.tro else (b.layout_id or b.bmk)
|
||||||
|
if b.tro and b.tro not in tros:
|
||||||
|
p.fehler("Zuordnung", f"{wo}: TRO {b.tro} steht nicht im Layout")
|
||||||
|
continue
|
||||||
|
if b.tro and (b.tro, b.slot) not in slots:
|
||||||
|
p.fehler("Zuordnung", f"{wo}: Slot {b.slot} gibt es an {b.tro} nicht")
|
||||||
|
continue
|
||||||
|
if b.status == "widerspruch":
|
||||||
|
p.fehler("Zuordnung", f"{wo}: {b.bmk or '-'} - {b.note or 'Widerspruch'}")
|
||||||
|
elif b.status == "offen":
|
||||||
|
offen[b.role] += 1
|
||||||
|
if geraete is not None and b.bmk and b.status != "widerspruch" and b.bmk not in geraete:
|
||||||
|
p.fehler("Zuordnung", f"{wo}: {b.bmk} fehlt in der Hardwareplanung")
|
||||||
|
if offen:
|
||||||
|
p.warnung("Zuordnung", "offene Pflichtrollen (TODO(E-Planung)): "
|
||||||
|
+ ", ".join(f"{r} {n}x" for r, n in sorted(offen.items())))
|
||||||
|
|
||||||
|
|
||||||
|
def _mtime(path: Path | None) -> float | None:
|
||||||
|
return path.stat().st_mtime if path is not None and path.is_file() else None
|
||||||
|
|
||||||
|
|
||||||
|
def _has_sections(path: Path, csv_name: str) -> bool:
|
||||||
|
"""Hat die INI einen Abschnitt [<csv>.*]?"""
|
||||||
|
if not path.is_file():
|
||||||
|
return False
|
||||||
|
prefix = f"[{csv_name.lower()}."
|
||||||
|
try:
|
||||||
|
return any(line.strip().lower().startswith(prefix)
|
||||||
|
for line in path.read_text(encoding="utf-8-sig").splitlines())
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_freshness(anlage_file: Path, anlage: AnlageDocument | None, tro_file: Path | None,
|
||||||
|
hw_file: Path | None, cfg_dir: Path, csv_name: str,
|
||||||
|
p: Pruefung) -> None:
|
||||||
|
"""Ist die JSON juenger als alles, woraus sie gebaut wird?"""
|
||||||
|
stand = _mtime(anlage_file)
|
||||||
|
if stand is None:
|
||||||
|
return
|
||||||
|
if anlage is not None:
|
||||||
|
hw_ini = cfg_dir / HW_OVERRIDES_FILE
|
||||||
|
# hw_overrides.ini zaehlt nur mit Abschnitten zu dieser Anlage
|
||||||
|
if not _has_sections(hw_ini, csv_name):
|
||||||
|
hw_ini = None
|
||||||
|
for quelle, was in ((tro_file, "TRO-JSON"), (hw_file, "Hardwareplanung"),
|
||||||
|
(hw_ini, HW_OVERRIDES_FILE)):
|
||||||
|
t = _mtime(quelle)
|
||||||
|
if t is not None and t > stand:
|
||||||
|
p.fehler("Stand", f"{anlage_file.name} ist aelter als {was} ({quelle.name}) "
|
||||||
|
f"- merge_hw_layout neu laufen lassen")
|
||||||
|
# BricsCAD-Korrekturen wirken in tro_flow - die TRO-JSON muss juenger sein
|
||||||
|
layout_stand = _mtime(tro_file) if anlage is not None else stand
|
||||||
|
korrektur = override_path(cfg_dir)
|
||||||
|
t = _mtime(korrektur)
|
||||||
|
if layout_stand is not None and t is not None and t > layout_stand:
|
||||||
|
try:
|
||||||
|
if load_overrides(csv_name, cfg_dir).edits:
|
||||||
|
p.warnung("Stand", f"{korrektur.name} (Handkorrekturen aus BricsCAD) ist "
|
||||||
|
f"juenger als die TRO-JSON - tro_flow --json neu laufen lassen")
|
||||||
|
except (OSError, configparser.Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def check_corrections(doc: TroDocument, cfg_dir: Path, csv_name: str, p: Pruefung) -> None:
|
||||||
|
"""Handkorrekturen, die ins Leere zeigen (z. B. nach einer Umbenennung)."""
|
||||||
|
hw_ini = cfg_dir / HW_OVERRIDES_FILE
|
||||||
|
if hw_ini.is_file():
|
||||||
|
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"),
|
||||||
|
interpolation=None)
|
||||||
|
parser.optionxform = str
|
||||||
|
try:
|
||||||
|
with hw_ini.open(encoding="utf-8-sig") as fh:
|
||||||
|
parser.read_file(fh)
|
||||||
|
except (OSError, configparser.Error) as exc:
|
||||||
|
p.fehler("Korrektur", f"{hw_ini.name} nicht lesbar: {exc}")
|
||||||
|
else:
|
||||||
|
section = f"{csv_name}.bindings"
|
||||||
|
for key in (parser[section] if parser.has_section(section) else {}):
|
||||||
|
teile = key.split(".")
|
||||||
|
tro = doc.resolve(teile[0]) if len(teile) == 3 else None
|
||||||
|
if tro is None or not any(s.slot == teile[1] for s in tro.slots):
|
||||||
|
p.warnung("Korrektur", f"{hw_ini.name} [{section}] {key}: kein TRO/Slot "
|
||||||
|
f"dazu (ID und Schluessel geprueft)")
|
||||||
|
jam_ini = cfg_dir / f"{Path(csv_name).stem}{JAM_NAMES_SUFFIX}"
|
||||||
|
try:
|
||||||
|
jam = load_jam_overrides(jam_ini)
|
||||||
|
except (OSError, ValueError, configparser.Error) as exc:
|
||||||
|
p.fehler("Korrektur", f"{jam_ini.name} nicht lesbar: {exc}")
|
||||||
|
return
|
||||||
|
if jam:
|
||||||
|
areas = doc.jam_areas_or_derived()
|
||||||
|
treffer = {k for a in areas for k in (jam_stable_key(a, doc), jam_key(a))}
|
||||||
|
for key in sorted(set(jam) - treffer):
|
||||||
|
p.warnung("Korrektur", f"{jam_ini.name} {key}: kein Staubereich dazu")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Gesamt
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def validate(json_file: Path, *, hw_file: Path | None = None,
|
||||||
|
results: Path | None = None, cfg_dir: Path | None = None) -> Pruefung:
|
||||||
|
"""TRO- oder Anlagen-JSON pruefen. Ladefehler werden zu einem FEHLER."""
|
||||||
|
results = results or json_file.parent
|
||||||
|
cfg_dir = cfg_dir or env_dir("SKEL_CFG", "cfg")
|
||||||
|
p = Pruefung()
|
||||||
|
try:
|
||||||
|
text = json_file.read_text(encoding="utf-8")
|
||||||
|
if json.loads(text).get("schema") == ANLAGE_SCHEMA:
|
||||||
|
anlage: AnlageDocument | None = AnlageDocument.from_json(text)
|
||||||
|
doc = anlage.layout
|
||||||
|
else:
|
||||||
|
anlage, doc = None, TroDocument.from_json(text)
|
||||||
|
except (OSError, ValueError, ValidationError) as exc:
|
||||||
|
p.fehler("Modell", f"{json_file.name} nicht lesbar oder passt nicht zum Modell: "
|
||||||
|
f"{str(exc).splitlines()[0]}")
|
||||||
|
return p
|
||||||
|
|
||||||
|
csv_name = Path(doc.source).name
|
||||||
|
check_structure(doc, p)
|
||||||
|
|
||||||
|
hw = None
|
||||||
|
tro_file = None
|
||||||
|
if anlage is not None:
|
||||||
|
tro_file = results / Path(anlage.layout_file).name if anlage.layout_file else None
|
||||||
|
if hw_file is None and anlage.hw_file:
|
||||||
|
hw_file = results / Path(anlage.hw_file).name
|
||||||
|
if hw_file is not None and hw_file.is_file():
|
||||||
|
try:
|
||||||
|
hw = HardwareDocument.from_json(hw_file.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, ValidationError) as exc:
|
||||||
|
p.fehler("Zuordnung", f"{hw_file.name} nicht lesbar: {str(exc).splitlines()[0]}")
|
||||||
|
elif hw_file is not None:
|
||||||
|
p.warnung("Zuordnung", f"{hw_file.name} nicht gefunden - BMK nicht gegengeprueft")
|
||||||
|
check_bindings(anlage, hw, p)
|
||||||
|
check_freshness(json_file, anlage, tro_file, hw_file, cfg_dir, csv_name, p)
|
||||||
|
check_corrections(doc, cfg_dir, csv_name, p)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def report(json_file: Path, p: Pruefung, strict: bool, out=sys.stdout) -> None:
|
||||||
|
print("", file=out)
|
||||||
|
print("================================================================", file=out)
|
||||||
|
print("PRUEFUNG ANLAGEN-JSON", file=out)
|
||||||
|
print("================================================================", file=out)
|
||||||
|
print(f"Datei = {json_file}", file=out)
|
||||||
|
print(f"Fehler = {p.anzahl_fehler}", file=out)
|
||||||
|
print(f"Warnungen = {p.anzahl_warnungen}", file=out)
|
||||||
|
print(f"Ergebnis = {'bereit fuer die SCL' if p.bereit(strict) else 'NICHT bereit'}"
|
||||||
|
+ (" (--strict: Warnungen zaehlen)" if strict else ""), file=out)
|
||||||
|
print("================================================================", file=out)
|
||||||
|
for b in p.befunde:
|
||||||
|
print(f" {b}", file=out)
|
||||||
|
print("", file=out)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="validate_anlage",
|
||||||
|
description="Prueft eine Anlagen-JSON (oder TRO-JSON), bevor daraus SCL wird.",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="Beispiele:\n"
|
||||||
|
" validate_anlage.bat --json mubea_anlage.json\n"
|
||||||
|
" validate_anlage.bat --json dematic_merge_anlage.json --strict\n"
|
||||||
|
"\nExit-Codes: 0 bereit, 1 Aufruffehler, 5 nicht bereit\n",
|
||||||
|
)
|
||||||
|
parser.add_argument("--json", required=True, metavar="NAME",
|
||||||
|
help="Anlagen-JSON (<csv>_anlage.json) oder TRO-JSON, in "
|
||||||
|
"%%SKEL_RESULTS%% oder ein Pfad.")
|
||||||
|
parser.add_argument("--hw", metavar="NAME",
|
||||||
|
help="Hardwareplanung (<projekt>_hw.json). Standard: hwFile "
|
||||||
|
"der Anlagen-JSON.")
|
||||||
|
parser.add_argument("--strict", action="store_true",
|
||||||
|
help="Auch Warnungen machen die JSON 'nicht bereit'.")
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def _in_results(name: str, results: Path) -> Path:
|
||||||
|
path = Path(name)
|
||||||
|
return results / name if not path.is_absolute() and path.parent == Path(".") else path
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
return 1
|
||||||
|
hw_file = _in_results(args.hw, results) if args.hw else None
|
||||||
|
p = validate(json_file, hw_file=hw_file, results=json_file.parent)
|
||||||
|
report(json_file, p, args.strict)
|
||||||
|
return 0 if p.bereit(args.strict) else EXIT_FEHLER
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Tests fuer den Ablauf Anlagen-JSON -> (BricsCAD) -> SCL.
|
||||||
|
|
||||||
|
- stabiler Schluessel (DerivedTro.key): Korrekturen ueberleben eine Umbenennung
|
||||||
|
- Verbindungen aus BricsCAD (edit_connections, tro_flow.apply_connection_edits,
|
||||||
|
tro_annotate._read_connections)
|
||||||
|
- Pruefung vor der SCL (validate_anlage)
|
||||||
|
- Zeichnung aus der JSON (tro_annotate.analysis_from_document)
|
||||||
|
|
||||||
|
Fixture: Testanlage test_system (wie test_tro_edits.py).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from material_flow import build_graph, read_elements
|
||||||
|
from model import AnlageDocument, Binding, TroDocument
|
||||||
|
from scl_context import jam_override, jam_stable_key
|
||||||
|
from tro_annotate import analysis_from_document, format_slots, read_tros, tro_slots
|
||||||
|
from tro_flow import analyse_tros
|
||||||
|
from tro_overrides import ConnectionEdit, TroEdit, load_overrides, save_edits
|
||||||
|
from validate_anlage import FEHLER, validate
|
||||||
|
|
||||||
|
FIXTURE = Path(__file__).parent / "fixtures" / "test_system_export.csv"
|
||||||
|
CSV = FIXTURE.name
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def layout():
|
||||||
|
elements = read_elements(FIXTURE, [])
|
||||||
|
return elements, build_graph(elements, CSV, [])
|
||||||
|
|
||||||
|
|
||||||
|
def _analyse(layout, edits=None, connection_edits=None):
|
||||||
|
elements, graph = layout
|
||||||
|
return analyse_tros(graph, elements, edits=edits, connection_edits=connection_edits)
|
||||||
|
|
||||||
|
|
||||||
|
def _tro_kante(analysis):
|
||||||
|
"""Eine abgeleitete Kante zwischen zwei TROs (keine Station)."""
|
||||||
|
ids = {t.tro_id for t in analysis.tros}
|
||||||
|
return next((s, d) for s, d in analysis.edges if s in ids and d in ids)
|
||||||
|
|
||||||
|
|
||||||
|
def _markers(analysis):
|
||||||
|
return [{"ID": t.tro_id, "KEY": analysis.edit_key(t.tro_id), "TYPE": t.type_name,
|
||||||
|
"UID": f"{n:04d}", "SLOTS": format_slots(*tro_slots(t)),
|
||||||
|
"_BLOCK": f"TRO_SYM_{t.type_name}"}
|
||||||
|
for n, t in enumerate(analysis.tros, start=100)]
|
||||||
|
|
||||||
|
|
||||||
|
def _arrows(analysis, markers):
|
||||||
|
uid = {m["ID"]: m["UID"] for m in markers}
|
||||||
|
ids = {t.tro_id for t in analysis.tros}
|
||||||
|
return [{"FROM_TRO": s, "TO_TRO": d, "FROM_UID": uid[s], "TO_UID": uid[d],
|
||||||
|
"KIND": "flow", "_BLOCK": "CONNECTION_ARROW"}
|
||||||
|
for s, d in analysis.edges if s in ids and d in ids]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Stabiler Schluessel
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_umbenannter_tro_traegt_seinen_schluessel(layout):
|
||||||
|
raw = _analyse(layout)
|
||||||
|
key = raw.tros[0].tro_id
|
||||||
|
doc = TroDocument.from_analysis(_analyse(layout, [TroEdit(key=key, name="Neu_1")]), CSV)
|
||||||
|
tro = doc.by_id("Neu_1")
|
||||||
|
assert tro.key == key and tro.stable_key == key
|
||||||
|
assert doc.resolve(key) is tro and doc.resolve("Neu_1") is tro
|
||||||
|
# Staubereichs-Korrektur unter dem alten Schluessel gilt weiter
|
||||||
|
area = next(a for a in doc.jam_areas_or_derived() if a.tro == "Neu_1")
|
||||||
|
assert jam_stable_key(area, doc) == f"{key}.{area.entry}"
|
||||||
|
assert jam_override(area, doc, {f"{key}.{area.entry}": ("0042", 3)}) == ("0042", 3)
|
||||||
|
# unveraenderte TROs: Schluessel = ID
|
||||||
|
assert all(t.key == t.tro_id for t in doc.tros if t.tro_id != "Neu_1")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Verbindungen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_edit_hin_und_zurueck(tmp_path):
|
||||||
|
c = ConnectionEdit.parse("+ TRO01 -> TRO05 | weiche")
|
||||||
|
assert (c.add, c.src, c.dst, c.kind) == (True, "TRO01", "TRO05", "weiche")
|
||||||
|
assert ConnectionEdit.parse(c.render()) == c
|
||||||
|
assert not ConnectionEdit.parse("- TRO01 -> TRO05").add
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ConnectionEdit.parse("TRO01 -> TRO05")
|
||||||
|
save_edits(CSV, [TroEdit(key="TRO02", name="X")], tmp_path,
|
||||||
|
[c, ConnectionEdit(add=False, src="TRO03", dst="TRO04")])
|
||||||
|
o = load_overrides(CSV, tmp_path)
|
||||||
|
assert o.connection_edits == [c, ConnectionEdit(add=False, src="TRO03", dst="TRO04")]
|
||||||
|
assert [e.key for e in o.edits] == ["TRO02"]
|
||||||
|
save_edits(CSV, [], tmp_path, [])
|
||||||
|
assert "edit_connections" not in (tmp_path / "tro_overrides.ini").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_verbindungen_aendern_vorgaenger_und_nachfolger(layout):
|
||||||
|
raw = _analyse(layout)
|
||||||
|
s, d = _tro_kante(raw)
|
||||||
|
andere = next(t.tro_id for t in raw.tros
|
||||||
|
if t.tro_id not in (s, d) and (t.tro_id, d) not in raw.edges)
|
||||||
|
analysis = _analyse(layout, connection_edits=[
|
||||||
|
ConnectionEdit(add=False, src=s, dst=d),
|
||||||
|
ConnectionEdit(add=True, src=andere, dst=d),
|
||||||
|
ConnectionEdit(add=True, src="GIBTSNICHT", dst=d)])
|
||||||
|
assert (s, d) not in analysis.edges and (andere, d) in analysis.edges
|
||||||
|
assert analysis.edge_kinds[(andere, d)] == "transfer"
|
||||||
|
ziel = analysis.by_id(d)
|
||||||
|
assert andere in ziel.predecessors and s not in ziel.predecessors
|
||||||
|
assert d not in analysis.by_id(s).successors
|
||||||
|
assert any("GIBTSNICHT gibt es nicht" in f for f in analysis.findings)
|
||||||
|
# die Staubereiche folgen den Vorgaengern
|
||||||
|
doc = TroDocument.from_analysis(analysis, CSV)
|
||||||
|
eingang = next(a for a in doc.jam_areas if a.tro == d and a.entry == 1)
|
||||||
|
assert andere in eingang.predecessors
|
||||||
|
|
||||||
|
|
||||||
|
def test_verbindung_ueber_schluessel_nach_umbenennung(layout):
|
||||||
|
raw = _analyse(layout)
|
||||||
|
s, d = _tro_kante(raw)
|
||||||
|
analysis = _analyse(layout, [TroEdit(key=s, name="Quelle_1")],
|
||||||
|
[ConnectionEdit(add=False, src=s, dst=d)])
|
||||||
|
assert ("Quelle_1", d) not in analysis.edges
|
||||||
|
|
||||||
|
|
||||||
|
def test_pfeile_der_zeichnung_werden_zu_verbindungskorrekturen(layout, tmp_path):
|
||||||
|
elements, _ = layout
|
||||||
|
raw = _analyse(layout)
|
||||||
|
markers = _markers(raw)
|
||||||
|
arrows = _arrows(raw, markers)
|
||||||
|
# unveraendert: keine Korrektur
|
||||||
|
_, meldungen = read_tros(markers, raw, elements, CSV, tmp_path, arrows=arrows)
|
||||||
|
assert load_overrides(CSV, tmp_path).connection_edits == []
|
||||||
|
|
||||||
|
s, d = _tro_kante(raw)
|
||||||
|
andere = next(t.tro_id for t in raw.tros
|
||||||
|
if t.tro_id not in (s, d) and (t.tro_id, d) not in raw.edges)
|
||||||
|
arrows = [a for a in arrows if (a["FROM_TRO"], a["TO_TRO"]) != (s, d)]
|
||||||
|
arrows.append({"FROM_TRO": andere, "TO_TRO": d, "FROM_UID": "", "TO_UID": "",
|
||||||
|
"KIND": "flow", "_BLOCK": "CONNECTION_ARROW"})
|
||||||
|
# ein Marker umbenannt, seine Pfeile tragen noch den alten Namen -> ueber UID
|
||||||
|
quelle = next(m for m in markers if m["ID"] == s)
|
||||||
|
quelle["ID"] = "Quelle_1"
|
||||||
|
read_tros(markers, raw, elements, CSV, tmp_path, arrows=arrows)
|
||||||
|
o = load_overrides(CSV, tmp_path)
|
||||||
|
assert o.connection_edits == [ConnectionEdit(add=False, src=s, dst=d),
|
||||||
|
ConnectionEdit(add=True, src=andere, dst=d)]
|
||||||
|
assert [(e.key, e.name) for e in o.edits] == [(s, "Quelle_1")]
|
||||||
|
|
||||||
|
# Rundlauf: angewandt, neu gezeichnet, wieder gelesen -> dasselbe
|
||||||
|
edited = _analyse(layout, o.edits, o.connection_edits)
|
||||||
|
m2 = _markers(edited)
|
||||||
|
again, _ = read_tros(m2, raw, elements, CSV, tmp_path, write=False,
|
||||||
|
arrows=_arrows(edited, m2), base=_analyse(layout, o.edits))
|
||||||
|
assert [(e.key, e.name) for e in again] == [(s, "Quelle_1")]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Pruefung
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _anlage_dateien(layout, tmp_path, bindings=None):
|
||||||
|
doc = TroDocument.from_analysis(_analyse(layout), CSV)
|
||||||
|
tro_file = tmp_path / "x_tro.json"
|
||||||
|
tro_file.write_text(doc.to_json(), encoding="utf-8")
|
||||||
|
anlage = AnlageDocument(layout=doc, layout_file=tro_file.name, bindings=bindings or [])
|
||||||
|
anlage_file = tmp_path / "x_anlage.json"
|
||||||
|
anlage_file.write_text(anlage.model_dump_json(by_alias=True, indent=2), encoding="utf-8")
|
||||||
|
return tro_file, anlage_file, doc
|
||||||
|
|
||||||
|
|
||||||
|
def test_pruefung_frische_anlage_ist_bereit(layout, tmp_path):
|
||||||
|
tro_file, anlage_file, _ = _anlage_dateien(layout, tmp_path)
|
||||||
|
os.utime(tro_file, (1_000_000, 1_000_000))
|
||||||
|
p = validate(anlage_file, cfg_dir=tmp_path)
|
||||||
|
assert p.anzahl_fehler == 0, [str(b) for b in p.befunde]
|
||||||
|
assert p.bereit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_pruefung_findet_fehler(layout, tmp_path):
|
||||||
|
tro = TroDocument.from_analysis(_analyse(layout), CSV).tros[0]
|
||||||
|
bindings = [Binding(tro="TRO99", slot="separator1", role="sensor", bmk="BG1"),
|
||||||
|
Binding(tro=tro.tro_id, slot="separator1", role="sensor", bmk="BG2",
|
||||||
|
status="widerspruch", note="BMK nicht im TIA-Import"),
|
||||||
|
Binding(tro=tro.tro_id, slot="separator1", role="stopper", status="offen")]
|
||||||
|
tro_file, anlage_file, _ = _anlage_dateien(layout, tmp_path, bindings)
|
||||||
|
# TRO-JSON juenger als die Anlagen-JSON -> veraltet
|
||||||
|
os.utime(anlage_file, (1_000_000, 1_000_000))
|
||||||
|
(tmp_path / "x_jam_names.ini").write_text("[jam]\nTRO99.1 = 0042_1\n", encoding="utf-8")
|
||||||
|
p = validate(anlage_file, cfg_dir=tmp_path)
|
||||||
|
texte = [b.text for b in p.befunde if b.stufe == FEHLER]
|
||||||
|
assert any("TRO99 steht nicht im Layout" in t for t in texte)
|
||||||
|
assert any("BMK nicht im TIA-Import" in t for t in texte)
|
||||||
|
assert any("aelter als TRO-JSON" in t for t in texte)
|
||||||
|
assert any("stopper 1x" in b.text for b in p.befunde)
|
||||||
|
assert not p.bereit()
|
||||||
|
# die Staubereichsdatei heisst nach der CSV, nicht nach der JSON
|
||||||
|
assert not any("TRO99.1" in b.text for b in p.befunde)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pruefung_unbekannte_verbindung(layout, tmp_path):
|
||||||
|
doc = TroDocument.from_analysis(_analyse(layout), CSV)
|
||||||
|
doc.connections[0].dst = "TRO99"
|
||||||
|
f = tmp_path / "x_tro.json"
|
||||||
|
f.write_text(doc.to_json(), encoding="utf-8")
|
||||||
|
p = validate(f, cfg_dir=tmp_path)
|
||||||
|
assert any("TRO99 unbekannt" in b.text for b in p.befunde if b.stufe == FEHLER)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Zeichnung aus der JSON
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_zeichnung_zeigt_die_json(layout):
|
||||||
|
raw = _analyse(layout)
|
||||||
|
s = raw.tros[0].tro_id
|
||||||
|
doc = TroDocument.from_analysis(_analyse(layout, [TroEdit(key=s, name="Neu_1")]), CSV)
|
||||||
|
a = analysis_from_document(doc)
|
||||||
|
assert [t.tro_id for t in a.tros] == [t.tro_id for t in doc.tros]
|
||||||
|
assert set(a.edges) == {(c.src, c.dst) for c in doc.connections}
|
||||||
|
assert a.edit_key("Neu_1") == s
|
||||||
|
assert [b.tro_id for b in a.boundaries] == [b.tro_id for b in doc.boundaries]
|
||||||
Reference in New Issue
Block a user