hw_import --dxf: gemeinsame Zeichnung (Layout + E-Planung) als Hardwarequelle

hw_import liest mit --dxf statt der Positionsdatei eine DXF, in der Layout
und E-Planung zusammen liegen (Dematic): jeder Block mit Attribut IO ist ein
E-Bauteil (IO/VERW/BEZEICHNUNG/KENNZEICHNUNG, Einfuegepunkt), schon in
Layout-Koordinaten (HardwareDocument.layoutCoordinates). --conveyor-from-motor
nimmt den Foerderer aus der Motornummer, wenn kein K0102/#106 dasteht
("Motor KR-MA0055" -> K0055); "ILS-VARIO" zaehlt als Strecke.

merge_hw_layout: bei layoutCoordinates kein Lagebezug (Identitaet, nichts
abgelegt); --rules findet einen blossen Namen in SKEL_CFG. Neue Regeldatei
cfg/e_planning_dematic.ini (Sensor/Stopper gleiche Nummer, Weichenventil =
Nummer des Stausensors, Abstaende an dematic_merge..dxf gemessen).

Fehler behoben: zwei Antriebs-TROs auf einem Layout-Foerderer (Dematic VF_6,
TRO04/TRO06) bekamen beide denselben Vario-Motor. Jetzt Widerspruch mit
Vorschlag; scl_context faellt dann nicht auf den Motor des Foerderers zurueck.

Dematic: 115 Namen im FB_Main (alle im TIA-Import), offene Stellen 730 -> 615.
cfg/dematic_merge_tro_ids.ini: TRO-IDs der Dematic-Anlage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 18:21:20 +02:00
parent d414bd6cf3
commit 2fbd2f119d
10 changed files with 467 additions and 38 deletions
+7 -2
View File
@@ -37,7 +37,8 @@ Template" below). What exists today is:
- `cfg/dxf_registration.json` — persisted, verified CSV↔DXF coordinate transform;
`cfg/tro_overrides.ini` — hand-maintained corrections to the derived TROs (see `tro_overrides.py`);
`cfg/e_planning.ini` — project-specific rules for assigning E-planning symbols to layout parts
(read by `merge_hw_layout.py`, also for `e_planning.py`);
(read by `merge_hw_layout.py`, also for `e_planning.py`); `cfg/e_planning_dematic.ini` — the
same for Dematic (`merge_hw_layout --rules e_planning_dematic.ini`);
`cfg/hw_overrides.ini` — hand corrections of `merge_hw_layout.py` (bindings per TRO slot role, conveyors);
`cfg/parameter.ini` — tolerances, thresholds, scales and drawing sizes of the `lib/` modules
(read by `lib/parameter.py`; no numeric settings in the code, see `doc/Python_Scripts.md` §7a);
@@ -221,7 +222,11 @@ byte — rewrite them (`SKEL_UPDATE_GOLDEN=1`) only for a reviewed, intended cha
`*_positionsconv.json` → `<projekt>_hw.json` (`HardwareDocument`): per BMK address,
index constant `cIn…`/`cOut…`, VERW, station, position, conveyor (`K0102`; `#106` →
`K0106`); drives MA↔FC (+ encoder GF); cross-check of both sources. EA list and WSCAD
are not needed. See `doc/Python_Scripts.md` §5d.
are not needed. Instead of the positions file `--dxf` reads a **merged drawing** (layout +
E planning in one DXF, Dematic): every block with an `IO` attribute is a device, already
in layout coordinates (`layoutCoordinates`, no registration in the merge);
`--conveyor-from-motor` takes the conveyor from the motor number (`MA0055` → `K0055`).
See `doc/Python_Scripts.md` §5d.
7. **`lib/merge_hw_layout.py`** (implemented, stage 3) — `<csv>_tro.json` + `<projekt>_hw.json`
→ `<csv>_anlage.json` (`model.AnlageDocument`: layout unchanged + `bindings[]` keyed by
TRO-ID + slot + role, `conveyors[]`). Reads no CSV: the TRO-JSON carries `parts`
+37
View File
@@ -0,0 +1,37 @@
; ============================================================
; TRO-IDs zu dematic_merge.csv - erzeugt von lib/tro_flow.py am 2026-10-21
;
; Je TRO: ID = x, y (mm) seines Anker-Separators im CSV-Koordinatensystem.
; Die IDs duerfen umbenannt werden (eindeutig, gueltiger SCL-Bezeichner);
; tro_flow findet jeden TRO ueber die Lage wieder. Neue TROs werden
; unten angehaengt, weggefallene bleiben stehen und werden gemeldet.
; Alles neu nummerieren: tro_flow --renumber (ueberschreibt diese Datei).
; Siehe doc/Konzept_Layout_Hardware_SCL.md, Abschnitt TRO-IDs.
; ============================================================
[tro]
TRO01 = 34716.5, 3406.7 ; Vario | VF_4 | Sep 0074
TRO02 = 20161.7, 3406.7 ; 1Sep | VF_4 | Sep 0075
TRO03 = 42164.0, 4329.5 ; 1Sep2Swi | Separator 0048 an Ausschleuselementen 0073, 0081 (Strecken 0008, 0010) | Sep 0048
TRO04 = 40437.8, 2448.3 ; Vario | VF_6 | Sep 0082
TRO05 = 33048.6, 2448.3 ; 1Sep | VF_6 | Sep 0083
TRO06 = 26860.8, 2448.3 ; Vario | VF_6 | Sep 0084
TRO07 = 20134.0, 2448.3 ; 1Sep | VF_6 | Sep 0085
TRO08 = 20161.9, 6635.8 ; Vario | VF_5 | Sep 0078
TRO09 = 20161.9, 11560.3 ; 1Sep | VF_5 | Sep 0079
TRO10 = 19604.0, 4982.3 ; 1Sep1Swi | Separator 0043 an Ausschleuselement 0077 (Strecke 0009, Anfang) | Sep 0043
TRO11 = 3098.7, 12119.7 ; 1Sep | Separator 0038 auf Kreisel2 Bahn L | Sep 0038
TRO12 = 34990.4, 12919.7 ; 1Sep | Separator 0046 auf Kreisel2 Bahn R | Sep 0046
TRO13 = 5034.1, 12919.7 ; PinStore_Auto | Speicher (10 Schwerkraftlinien) | Sep 0040, 0088, 0091, 0094, 0097, 0100, 0103, 0106, 0109, 0112, 0115
TRO14 = 35122.8, 25278.8 ; 1Sep | Separator 0047 auf Kreisel3 Bahn R | Sep 0047
TRO15 = 2703.7, 21969.7 ; Vario | VF_1 | Sep 0062
TRO16 = 2703.7, 13476.7 ; 1Sep | VF_1 | Sep 0063
TRO17 = 3542.3, 24478.8 ; 1Sep1Swi | Separator 0039 an Ausschleuselement 0061 (Strecke 0005, Anfang) | Sep 0039
TRO18 = 14674.7, 21969.7 ; Vario | VF_2 | Sep 0066
TRO19 = 14674.7, 13476.7 ; 1Sep | VF_2 | Sep 0067
TRO20 = 15472.4, 24478.8 ; 1Sep1Swi | Separator 0041 an Ausschleuselement 0065 (Strecke 0006, Anfang) | Sep 0041
TRO21 = 23232.7, 21969.7 ; Vario | VF_3 | Sep 0070
TRO22 = 23232.7, 13476.7 ; 1Sep | VF_3 | Sep 0071
TRO23 = 24062.0, 24478.8 ; 1Sep1Swi | Separator 0044 an Ausschleuselement 0069 (Strecke 0007, Anfang) | Sep 0044
TRO24 = 15934.1, 12919.8 ; PinStore_Auto | Speicher (7 Schwerkraftlinien) | Sep 0042, 0118, 0121, 0124, 0127, 0130, 0133, 0136
TRO25 = 24734.2, 12919.8 ; PinStore_Auto | Speicher (10 Schwerkraftlinien) | Sep 0045, 0139, 0142, 0145, 0148, 0151, 0154, 0157, 0160, 0163, 0166
+56
View File
@@ -0,0 +1,56 @@
; ================================================================
; e_planning_dematic.ini - Zuordnungsregeln fuer Dematic
; ================================================================
; Wie e_planning.ini (Aufbau und Schluessel siehe dort), aber fuer die
; gemeinsame Zeichnung Layout + E-Planung (hw_import --dxf). Aufruf:
; merge_hw_layout.bat --tro dematic_merge_tro.json --hw dematic_hw.json
; --rules e_planning_dematic.ini
;
; Die E-Symbole liegen schon in Layout-Koordinaten, aber neben dem Bauteil:
; die Abstaende unten sind Zeichenabstaende, gemessen 2026-10-21 an
; dematic_merge..dxf + dematic_merge_export.csv (Median je Rolle in Klammern).
; Unterschiede zu Mubea:
; - Sensor und Stopper haben dieselbe Nummer (BG3151 / MB3151)
; - Stausensor und Weichenventil einer Speicherlinie haben dieselbe Nummer
; (Stau 1 = BG3031 / Weiche Abzw. 1 = MB3031)
; - VERW-Texte "In Sep n", "Sep n", "Stau n", "Weiche Abzw. n"
; ================================================================
[Registrierung]
; wird bei der gemeinsamen Zeichnung nicht gebraucht (kein Lagebezug)
anker = IN SEP*
; Sensor im Stopper -> stInSeparator<n>.stSenInSep
; (Speicherlinien 568/609 mm, Kreisel/VF 220-1340 mm; VF_1 0062 1731 mm.)
; Zugeordnet wird eins zu eins, kleinster Abstand zuerst.
[Separator.sensor]
verw = IN SEP*
praefix = BG
max_mm = 1800
; Stopperventil -> xOutStopper<n>: gleiche Nummer wie der Sensor; das
; Symbol liegt an derselben Stelle
[Separator.stopper]
verw = SEP*
praefix = MB
nummer_von = Separator.sensor
nummer_versatz = 0
max_mm = 1800
; Stausensor am Anfang einer Speicherlinie (537-554 mm vom Ausschleuselement);
; haengt an keinem TRO-Slot, dient nur als Partner fuer die Nummernregel
[Ausschleus.stau]
verw = STAU*
praefix = BG
max_mm = 1000
soll_mm = 540
; Weichenventil am Ausschleuselement -> Slot switch<n>.valve (xOutSwitch).
; Speicherlinie: Nummer = Stausensor; Kreisel-Weichen ohne Stausensor:
; raeumlich (408-865 mm, Kreisel4 0081 1015 mm)
[Ausschleus.valve]
verw = WEICHE*
praefix = MB
nummer_von = Ausschleus.stau
nummer_versatz = 0
max_mm = 1400
+52 -9
View File
@@ -746,9 +746,19 @@ positions file exists.
```
*_TIA.xlsx (PLC Tags + Constants) ─┐
├─► hw_import.py ──► <project>_hw.json (HardwareDocument, sps_skel-hw-v1)
*_positionsconv.json (optional) ────┘ ──► <project>_hw.md (report: devices, drives, checks)
*_positionsconv.json (optional) ────┤ ──► <project>_hw.md (report: devices, drives, checks)
or merged drawing *.dxf (--dxf) ───┘
```
- **Merged drawing** (`--dxf`, instead of `--positions`; Dematic): layout and
E planning in one DXF. Every model-space block with a BMK in the attribute
`IO` is an E device (`IO` → BMK, `VERW`, `BEZEICHNUNG`, `KENNZEICHNUNG`,
insert point → position). Blocks without `IO` (layout parts, bus modules,
the green motor labels `ILS_Motorkennzeichnung*`) are ignored — the motor
symbol is authoritative. The positions are already layout coordinates;
`layoutCoordinates: true` tells `merge_hw_layout` to skip the registration.
A BMK drawn twice is a warning (the entry with `VERW` wins).
- **TIA import**: sheets are recognised by their header row, not their name
(English and German TIA exports): a sheet with `Logical Address` /
`Logische Adresse` holds tags, one with `Value` / `Wert` constants. Read with
@@ -762,6 +772,10 @@ positions file exists.
`K0104` (kind `Kreisel`); `CONVEYOR-#103` and `CONVEYOR-K0103` are the same
conveyor and both become `K0103` (kind `Strecke`) — the four-digit conveyor
number of the SCL naming scheme (`DB_JamArea0103`, `fbCarousel0102`).
With `--conveyor-from-motor` (Dematic) a text without `K…`/`#…` that names a
motor gives the conveyor by motor number: `Motor KR-MA0055`, `Rad dreht
ILS-KR (MA0046)` → `K0055`, `K0046`; `ILS-VARIO` counts as kind `Strecke`.
Not for Mubea, where motor `MA0203` drives conveyor `K0103`.
- **Drives**: motor `MA` ↔ motor protection `FC` (same number), conveyor and
kind from their texts, encoder `GF…` of the same conveyor.
- **Cross-check** (`checks`, also printed as `?` lines): BMK only in the
@@ -772,7 +786,8 @@ positions file exists.
through.
Switches: `--tia NAME` (required), `--positions NAME` (optional; without it no
positions), `--project NAME` (default from the positions file name:
positions), `--dxf NAME` (merged drawing instead of `--positions`),
`--conveyor-from-motor`, `--project NAME` (default from the positions file name:
`Mubea_positionsconv.json` → `Mubea`). Names resolve against `%SKEL_DATA%`,
also with a sub-folder. Exit `0` (findings are in the report), `1` on input or
read errors.
@@ -782,10 +797,17 @@ Result on Mubea (`mubea-1_TIA.xlsx` + `Mubea_positionsconv.json`): 238 devices,
11 drives all with motor protection and conveyor (encoder `GF20000` at K0102);
checks: `QA7000` only in the positions file, `BG0514` without controller.
Result on Dematic (`dematic_hw-2_TIA.xlsx` + `dematic_merge..dxf`,
`--conveyor-from-motor`): 1022 devices, 278 from the drawing, 267 in both
sources; 44 drives, all with motor protection and conveyor. Only in the drawing:
the network devices `BX2100–2108` (scanners), `GF2200` (encoder), `BC2050`
(RFID). 378 field devices only in TIA (areas not drawn).
Tests: `tests/test_hw_import.py` (xlsx reader with shared/inline strings,
header-based sheet detection, conveyor texts, merge per BMK, drives, every
cross-check, TIA-only run, JSON round trip, CLI with a sub-folder in
`%SKEL_DATA%`). The test builds its own small TIA xlsx.
header-based sheet detection, conveyor texts incl. motor numbers, merge per
BMK, drives, every cross-check, TIA-only run, JSON round trip, CLI with a
sub-folder in `%SKEL_DATA%`, merged drawing). The test builds its own small TIA
xlsx and DXF.
## 5e. `merge_hw_layout.py` — merge layout and hardware planning into the plant JSON
@@ -809,8 +831,14 @@ Steps:
layout separators), stored with the SHA-256 of the positions file. Warning
when fewer than `registration_min_share` of the separators find a partner —
a sign that layout and E drawing are different planning states.
A merged drawing (`layoutCoordinates`, `hw_import --dxf`) needs none:
identity, nothing stored. The symbols' offset beside their part (Dematic
~600 mm) is drawing distance, covered by the rules' `max_mm`/`soll_mm`.
2. **Rules** from `e_planning.ini`, unchanged logic (spatial with maximum /
target distance, numbering rule `MB = BG + 1`).
target distance, numbering rule `MB = BG + 1`). Per project another file
via `--rules` — Dematic: `cfg/e_planning_dematic.ini` (`In Sep n` / `Sep n`
with the same number, `Weiche Abzw. n` = number of `Stau n`, distances
measured on the merged drawing).
3. **Slots**: each assignment goes to the slot role of the TRO that holds the
part — key **TRO-ID + slot + role**. A `PinStore_Auto` now has a slot per
storage line (`line1` … `lineN`, same split as `scl_context._store`);
@@ -828,7 +856,11 @@ Steps:
layout conveyor. This gives motor, motor protection and encoder per layout
conveyor (`AnlageDocument.conveyor_for("0010-R")` → the TROs on that
Kreisel, i.e. `xInCarouselRun`) and fills `drive.motor` /
`drive.motorProtection` of Vario TROs.
`drive.motorProtection` of Vario TROs. When several TROs have a drive slot
on the same layout conveyor (Dematic VF_6: two Vario TROs), the drive is a
`widerspruch` with the conveyor's motor as a suggestion — each TRO has its
own motor; set it in `hw_overrides.ini`. `scl_skeleton` then keeps
`MA____` instead of falling back to the conveyor's motor.
5. **Corrections** from `hw_overrides.ini` (last):
`[<csv>.bindings] TRO01.separator1.sensor = BG0532` (or `= -` to clear) and
`[<csv>.conveyors] K0102 = 0010`.
@@ -838,7 +870,8 @@ Steps:
Every binding carries `constant` and `address` from the TIA import.
Switches: `--tro NAME`, `--hw NAME` (both required, resolved against
`%SKEL_RESULTS%`), `--rules FILE`, `--overrides FILE`, `--re-register`. Exit
`%SKEL_RESULTS%`), `--rules FILE` (a bare name resolves against `%SKEL_CFG%`),
`--overrides FILE`, `--re-register`. Exit
`0` also when not everything was assigned; `1` on input errors, an old
TRO-JSON without `parts`, or an unprovable registration. Parameters:
`cfg/parameter.ini [merge_hw_layout]`.
@@ -852,6 +885,14 @@ K0105 = Kreisel1 (2/3)**, each with motor and motor protection. The other
conveyors (Aufgabestation #100/#101, Vario #106–#109) are outside the CSV or
have a single vote and stay open.
Result on Dematic (`dematic_merge_tro.json` + `dematic_hw.json`, rules
`e_planning_dematic.ini`): 113 slot roles assigned (all 27 storage lines with
sensor and stopper, 23 of 25 separators, 5 of 6 switch valves, 6 Vario drives),
39 open (Vario sensors, scanners, VF_4/VF_6 where mechanical parts are still
missing), 4 contradictions (the two Vario TROs on VF_6). Then
`scl_skeleton --templates --strict`: 115 names inserted, all known in TIA,
open gaps 730 → 615.
**Stage 4:** `scl_skeleton --json <csv>_anlage.json` reads the plant JSON
directly (§5b). `--eplan` still works for a TRO-JSON and is phased out together
with `e_planning.py`.
@@ -860,7 +901,9 @@ Tests: `tests/test_merge_hw_layout.py` (sensor spatial / stopper by number,
contradiction, open roles, part without slot, conveyor majority with
overlapping boxes, Vario drive, too few votes, corrections incl. moving a BMK
and clearing a conveyor, storage-line slots, missing `parts`, JSON round trip,
fingerprint of the stored registration, CLI end to end with a real offset).
fingerprint of the stored registration, CLI end to end with a real offset,
merged drawing without registration, two drive TROs on one conveyor, Dematic
rules file).
## 6. `tro_catalog.py` — TRO type catalog (library, no CLI)
+119 -16
View File
@@ -16,13 +16,19 @@ genuegen (Befund doc/E-Planung_Bruecke.md, "Nachpruefung 2026-10-15"):
Kabellaengen-Tools (optional) - je BMK die Lage in der E-Zeichnung, die
Verwendung (VERW, z. B. "IN SEP"), Klartext und E/A-Station.
* **Gemeinsame Zeichnung** ``*.dxf`` (statt der Positionsdatei) - Layout
und E-Planung in einer DXF (Dematic): jeder Block mit Attribut ``IO`` ist
ein E-Bauteil (IO = BMK, VERW, BEZEICHNUNG, KENNZEICHNUNG, Einfuegepunkt).
Die Lage steht schon in Layout-Koordinaten; merge_hw_layout ermittelt
dann keinen Lagebezug (``layoutCoordinates``).
EA-Liste und WSCAD-Import bringen bei vorhandener Positionsdatei nichts
dazu und werden nicht gelesen.
Ergebnis ``HardwareDocument`` (Schema sps_skel-hw-v1): je BMK ein Geraet mit
beiden Seiten, die Antriebe (Motor MA <-> Motorschutz FC, Geber GF) je
Foerderer der E-Planung (K0102; "#106" wird K0106), und die Gegenpruefung
beider Quellen.
Foerderer der E-Planung (K0102; "#106" wird K0106; mit --conveyor-from-motor
auch "MA0055" -> K0055), und die Gegenpruefung beider Quellen.
Die Zuordnung zum Layout macht merge_hw_layout.py.
Die xlsx-Datei wird ohne Fremdbibliothek gelesen (zipfile + XML): der
@@ -59,6 +65,7 @@ __all__ = [
"read_xlsx",
"load_tia",
"load_positionsdraw",
"load_dxf",
"pair_drives",
"conveyor_of",
"conveyor_kind_of",
@@ -79,10 +86,13 @@ _ADDRESS = re.compile(r"^%?([EIAQM])\s*(\d+)\.(\d)$", re.IGNORECASE)
_CONVEYOR_K = re.compile(r"(?<![A-Z0-9])K(\d{4})(?!\d)")
# ... bzw. "CONVEYOR #100", "CONVEYOR-#106", "CONVEYOR#110"
_CONVEYOR_HASH = re.compile(r"#\s*(\d{3})(?!\d)")
# ... bzw. ueber den Motor (Dematic, nur mit motor=True): "Motor KR-MA0055",
# "Rad dreht ILS-KR (MA0046)", "ILS-VARIO-MA0043" -> K0055 / K0046 / K0043
_CONVEYOR_MA = re.compile(r"(?<![A-Z0-9])MA(\d{4})(?!\d)")
# Art des Foerderers im Klartext: "KREISEL-K0102", "KR-K0104" -> Kreisel;
# "CONVEYOR #106", "KLINKENFOERDERER" -> Strecke
# "CONVEYOR #106", "KLINKENFOERDERER", "ILS-VARIO" -> Strecke
_KIND_KREISEL = re.compile(r"(?<![A-Z])(KREISEL|KR)(?![A-Z])")
_KIND_STRECKE = re.compile(r"CONVEYOR|CONVEYER|F(OE|Ö)RDERER")
_KIND_STRECKE = re.compile(r"CONVEYOR|CONVEYER|F(OE|Ö)RDERER|(?<![A-Z])VARIO(?![A-Z])")
# ---------------------------------------------------------------------------
@@ -207,6 +217,9 @@ class HardwareDocument(BaseModel):
tia_sha256: str = Field(default="", alias="tiaSha256")
positions_file: str = Field(default="", alias="positionsFile")
positions_sha256: str = Field(default="", alias="positionsSha256")
# True: die Lage kommt aus der gemeinsamen Zeichnung (load_dxf) und steht
# schon in Layout-Koordinaten - merge_hw_layout ermittelt keinen Lagebezug
layout_coordinates: bool = Field(default=False, alias="layoutCoordinates")
devices: list[HwDevice] = Field(default_factory=list)
drives: list[Drive] = Field(default_factory=list)
# Konstanten ohne passendes Signal (z. B. Projektkonstanten)
@@ -416,6 +429,44 @@ def load_positionsdraw(path: Path, warnings: list[str] | None = None) -> list[ED
return list(devices.values())
def load_dxf(path: Path, warnings: list[str] | None = None) -> list[EDevice]:
"""E-Bauteile aus einer gemeinsamen Zeichnung (Layout + E-Planung) lesen.
Jeder Block im Modellbereich mit einem BMK im Attribut ``IO`` ist ein
E-Bauteil; die Lage ist der Einfuegepunkt - schon in Layout-Koordinaten.
Bloecke ohne IO (Layout-Bauteile, Busverteiler, die Motorkennzeichnung
"ILS-Kreisel MA0053") zaehlen nicht: der Motor steht im Motorsymbol.
Steht ein BMK doppelt da, gewinnt der Eintrag mit VERW.
"""
import ezdxf
warnings = warnings if warnings is not None else []
doc = ezdxf.readfile(str(path))
devices: dict[str, EDevice] = {}
count: Counter = Counter()
for insert in doc.modelspace().query("INSERT"):
attrs = {a.dxf.tag.upper(): a.dxf.text.strip() for a in insert.attribs}
bmk = attrs.get("IO", "")
if not _BMK.match(bmk):
continue
count[bmk] += 1
dev = EDevice(
bmk=bmk, verw=attrs.get("VERW", ""), bezeichnung=attrs.get("BEZEICHNUNG", ""),
kennzeichnung=attrs.get("KENNZEICHNUNG", ""),
pos=(float(insert.dxf.insert.x), float(insert.dxf.insert.y)))
old = devices.get(bmk)
if old is None or (not old.verw and dev.verw):
devices[bmk] = dev
for bmk, n in sorted(count.items()):
if n > 1:
warnings.append(f"Zeichnung: {bmk} steht {n}x in der Zeichnung "
f"- verwendet wird ein Eintrag")
if not devices:
raise ValueError(f"{path.name}: kein Block mit BMK im Attribut 'IO' gefunden "
f"- ist das die Zeichnung mit der E-Planung?")
return list(devices.values())
def _positions_warnings(path: Path) -> list[str]:
"""Die Hinweise, die das Kabellaengen-Tool selbst in die Datei schreibt."""
data = json.loads(path.read_text(encoding="utf-8"))
@@ -433,13 +484,18 @@ def _positions_warnings(path: Path) -> list[str]:
# ---------------------------------------------------------------------------
def conveyor_of(*texts: str) -> str:
def conveyor_of(*texts: str, motor: bool = False) -> str:
"""Foerderer der E-Planung aus einem Klartext als "K<NNNN>", sonst "".
Die E-Planung schreibt denselben Foerderer zweifach: "CONVEYOR-K0103"
(Motorschutz FC0203) und "CONVEYOR-#103" (Motor MA0203). Beides ist die
vierstellige Foerderernummer des SCL-Namensschemas (DB_JamArea0103,
fbCarousel0102) und wird zu "K0103".
``motor=True`` (Dematic): ohne K/# nennt der Klartext nur den Motor
("Motor KR-MA0055", "Rad dreht ILS-KR (MA0046)"); dann ist die
Motornummer die Foerderernummer -> "K0055". Bei Mubea waere das falsch
(Motor MA0203 treibt K0103), deshalb nur auf Wunsch.
"""
for text in texts:
upper = (text or "").upper()
@@ -449,6 +505,11 @@ def conveyor_of(*texts: str) -> str:
m = _CONVEYOR_HASH.search(upper)
if m:
return f"K{int(m.group(1)):04d}"
if motor:
for text in texts:
m = _CONVEYOR_MA.search((text or "").upper())
if m:
return f"K{m.group(1)}"
return ""
@@ -497,10 +558,18 @@ def _constant_name(bmk: str, direction: str) -> str:
def build_hardware(tia_path: Path | None, positions_path: Path | None,
project: str = "") -> HardwareDocument:
"""Beide Quellen einlesen, je BMK zusammenfuehren und gegeneinander pruefen."""
if tia_path is None and positions_path is None:
raise ValueError("weder TIA-Import noch Positionsdatei angegeben")
project: str = "", dxf_path: Path | None = None,
conveyor_from_motor: bool = False) -> HardwareDocument:
"""Beide Quellen einlesen, je BMK zusammenfuehren und gegeneinander pruefen.
Die Lage kommt aus der Positionsdatei oder aus der gemeinsamen Zeichnung
``dxf_path`` (load_dxf), nicht aus beiden. ``conveyor_from_motor``: siehe
conveyor_of(motor=True).
"""
if tia_path is None and positions_path is None and dxf_path is None:
raise ValueError("weder TIA-Import noch Positionsdatei/Zeichnung angegeben")
if positions_path is not None and dxf_path is not None:
raise ValueError("Positionsdatei und Zeichnung zugleich - nur eine Quelle fuer die Lage")
warnings: list[str] = []
checks: list[str] = []
@@ -512,6 +581,9 @@ def build_hardware(tia_path: Path | None, positions_path: Path | None,
if positions_path is not None:
e_devices = load_positionsdraw(positions_path, warnings)
warnings.extend(_positions_warnings(positions_path))
elif dxf_path is not None:
e_devices = load_dxf(dxf_path, warnings)
positions_path = dxf_path
tag_of = {}
for t in tags:
@@ -546,7 +618,8 @@ def build_hardware(tia_path: Path | None, positions_path: Path | None,
comment=tag.comment if tag else "",
constant=cname if const else "",
constant_value=value,
conveyor=conveyor_of(e.bezeichnung if e else "", tag.comment if tag else ""),
conveyor=conveyor_of(e.bezeichnung if e else "", tag.comment if tag else "",
motor=conveyor_from_motor),
conveyor_kind=conveyor_kind_of(e.bezeichnung if e else "",
tag.comment if tag else ""),
sources=[s for s, ok in (("tia", tag is not None), ("positions", e is not None))
@@ -561,6 +634,7 @@ def build_hardware(tia_path: Path | None, positions_path: Path | None,
tia_sha256=file_fingerprint(tia_path) if tia_path else "",
positions_file=positions_path.name if positions_path else "",
positions_sha256=file_fingerprint(positions_path) if positions_path else "",
layout_coordinates=dxf_path is not None,
devices=devices,
drives=pair_drives(devices),
constants=[c for c in constants if c.name not in used_constants],
@@ -627,7 +701,10 @@ def render_report(doc: HardwareDocument) -> str:
add(f"# Hardwareplanung: {doc.project}")
add("")
add(f"- TIA-Import: `{doc.tia_file or '-'}`")
add(f"- Positionsdatei: `{doc.positions_file or '-'}`")
if doc.layout_coordinates:
add(f"- Gemeinsame Zeichnung (Lage in Layout-Koordinaten): `{doc.positions_file}`")
else:
add(f"- Positionsdatei: `{doc.positions_file or '-'}`")
add(f"- Erzeugt: {doc.created}")
add("")
add("## Geraete")
@@ -699,20 +776,30 @@ def project_name(tia_path: Path | None, positions_path: Path | None) -> str:
def main(argv: list[str] | None = None) -> int:
import ezdxf
from material_flow import env_dir, resolve_input
args = parse_args(argv)
tia_path = resolve_input(args.tia) if args.tia else None
positions_path = resolve_input(args.positions) if args.positions else None
for label, path in (("TIA-Import", tia_path), ("Positionsdatei", positions_path)):
dxf_path = resolve_input(args.dxf) if args.dxf else None
if positions_path is not None and dxf_path is not None:
print("FEHLER: --positions und --dxf zugleich - nur eine Quelle fuer die Lage",
file=sys.stderr)
return 1
for label, path in (("TIA-Import", tia_path), ("Positionsdatei", positions_path),
("Zeichnung", dxf_path)):
if path is not None and not path.is_file():
print(f"FEHLER: {label} nicht gefunden: {path}", file=sys.stderr)
return 1
project = args.project or project_name(tia_path, positions_path)
project = args.project or project_name(tia_path, positions_path or dxf_path)
try:
doc = build_hardware(tia_path, positions_path, project)
except (OSError, ValueError, KeyError, zipfile.BadZipFile, ET.ParseError) as exc:
doc = build_hardware(tia_path, positions_path, project, dxf_path,
args.conveyor_from_motor)
except (OSError, ValueError, KeyError, zipfile.BadZipFile, ET.ParseError,
ezdxf.DXFError) as exc:
print(f"FEHLER: {exc}", file=sys.stderr)
return 1
@@ -730,7 +817,10 @@ def main(argv: list[str] | None = None) -> int:
print("HARDWAREPLANUNG EINLESEN")
print("================================================================")
print(f"TIA-Import = {tia_path or '-'}")
print(f"Positionsdatei = {positions_path or '-'}")
if dxf_path is not None:
print(f"Zeichnung = {dxf_path} (Lage in Layout-Koordinaten)")
else:
print(f"Positionsdatei = {positions_path or '-'}")
print(f"Projekt = {project}")
print(f"Geraete = {len(doc.devices)} (TIA {n_tia}, Positionsdatei {n_pos}, "
f"beide {sum(1 for d in doc.devices if len(d.sources) == 2)})")
@@ -763,6 +853,8 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
" hw_import.bat --tia Mubea/mubea-1_TIA.xlsx "
"--positions Mubea/Mubea_positionsconv.json\n"
" hw_import.bat --tia mubea-1_TIA.xlsx --project Mubea\n"
" hw_import.bat --tia dematic_hw-2_TIA.xlsx --dxf dematic_merge..dxf "
"--project dematic --conveyor-from-motor\n"
"\n"
"Exit-Codes:\n"
" 0 ok (Befunde der Gegenpruefung stehen im Bericht)\n"
@@ -774,6 +866,17 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser.add_argument("--positions", metavar="NAME",
help="*_positionsconv.json bzw. *_positionsdraw.json in "
"%%SKEL_DATA%% (oder ein Pfad); ohne sie keine Lage")
parser.add_argument("--dxf", metavar="NAME",
help="statt --positions: gemeinsame Zeichnung (Layout + "
"E-Planung, *.dxf) in %%SKEL_DATA%% (oder ein Pfad); "
"jeder Block mit Attribut IO ist ein E-Bauteil, die Lage "
"steht schon in Layout-Koordinaten")
parser.add_argument("--conveyor-from-motor", dest="conveyor_from_motor",
action="store_true",
help="Foerderer auch aus der Motornummer im Klartext "
"(\"Motor KR-MA0055\" -> K0055), wenn kein K0102/#106 "
"dasteht (Dematic). Nicht fuer Mubea: dort treibt MA0203 "
"den Foerderer K0103.")
parser.add_argument("--project", metavar="NAME",
help="Projektname fuer <projekt>_hw.json. Standard: aus dem "
"Namen der Positionsdatei (Mubea_positionsconv.json -> Mubea)")
+48 -6
View File
@@ -371,6 +371,19 @@ def resolve_transform(points, devices, anchors, cfg_dir: Path, csv_name: str,
return transform
def _same_drawing(points: list[LayoutPoint], drawing: str) -> Transform:
"""Lagebezug einer gemeinsamen Zeichnung (hw_import --dxf): keiner.
Die E-Symbole stehen schon in Layout-Koordinaten. Ihr Versatz neben dem
Bauteil (Dematic ~600 mm) ist Zeichenabstand, keine Verschiebung - ein
eingepasster Bezug wuerde ihn nur verzerren. Er steckt in max_mm/soll_mm
der Regeln.
"""
n = sum(1 for p in points if p.kind == "Separator")
return Transform(dx=0.0, dy=0.0, source="zeichnung", anchor="gemeinsame Zeichnung",
matched=n, candidates=n, dxf_file=drawing)
# ---------------------------------------------------------------------------
# Slots
# ---------------------------------------------------------------------------
@@ -529,9 +542,20 @@ def _conveyor_kinds(hw: HardwareDocument) -> dict[str, str]:
def bind_drives(doc: TroDocument, conveyors: list[ConveyorBinding],
hw: HardwareDocument) -> list[Binding]:
"""Antriebs-Slots (Vario): Motor und Motorschutz des zugeordneten Foerderers."""
"""Antriebs-Slots (Vario): Motor und Motorschutz des zugeordneten Foerderers.
Tragen mehrere TROs einen Antriebs-Slot auf demselben Layout-Foerderer
(Dematic VF_6: zwei Vario-TROs, drei Vario-Motoren), hat jeder TRO seinen
eigenen Motor - welcher, sagt die Mehrheit je Foerderer nicht. Dann wird
der Antrieb ein Widerspruch (BMK als Vorschlag), Korrektur von Hand.
"""
devices = hw.by_bmk()
by_layout = {c.layout_id: c for c in conveyors if c.status == "zugeordnet"}
sharing: dict[str, list[str]] = {}
for tro in doc.tros:
for s in tro.slots:
if s.layout_id and any(r.rstrip("?") in _DRIVE_ROLES for r in s.roles):
sharing.setdefault(s.layout_id.split("-", 1)[0], []).append(tro.tro_id)
bindings = []
for tro in doc.tros:
for s in tro.slots:
@@ -541,17 +565,24 @@ def bind_drives(doc: TroDocument, conveyors: list[ConveyorBinding],
if conv is None:
continue
roles = [r.rstrip("?") for r in s.roles]
shared = sharing.get(s.layout_id.split("-", 1)[0], [])
for role, attr in _DRIVE_ROLES.items():
bmk = getattr(conv, attr)
if role not in roles or not bmk:
continue
dev = devices.get(bmk)
status = "zugeordnet" if dev and dev.in_tia else "widerspruch"
note = f"Antrieb von {conv.eplan_name}"
if len(shared) > 1:
status = "widerspruch"
note = (f"{conv.layout_name or conv.layout_id} traegt {len(shared)} "
f"Antriebs-TROs ({', '.join(shared)}) - Antrieb je TRO von Hand "
f"({OVERRIDES_FILE}); Vorschlag {conv.eplan_name}")
bindings.append(Binding(
tro=tro.tro_id, slot=s.slot, role=role, layout_id=s.layout_id,
bmk=bmk, constant=dev.constant if dev else "",
address=dev.address if dev else "", source="positionsdraw",
rule="mehrheit", status="zugeordnet" if dev and dev.in_tia else "widerspruch",
note=f"Antrieb von {conv.eplan_name}"))
rule="mehrheit", status=status, note=note))
return bindings
@@ -690,7 +721,9 @@ def build_anlage(doc: TroDocument, hw: HardwareDocument, rules: list[Rule],
assignments: list[Assignment] = []
conveyors: list[ConveyorBinding] = []
if hw.field_devices():
if transform is None:
if transform is None and hw.layout_coordinates:
transform = _same_drawing(points, hw.positions_file)
elif transform is None:
transform = resolve_transform(points, devices, anchors, cfg_dir, doc.source,
hw.positions_file, re_register, warnings,
hw.positions_sha256)
@@ -856,6 +889,14 @@ def _in_results(name: str, results: Path) -> Path:
return results / name
def _in_cfg(name: str, cfg_dir: Path) -> Path:
"""Regeldatei: ein blosser Name, der hier nicht liegt, steht in %SKEL_CFG%."""
path = Path(name)
if path.is_absolute() or path.parent != Path(".") or path.is_file():
return path
return cfg_dir / name
def main(argv: list[str] | None = None) -> int:
from material_flow import env_dir
@@ -864,7 +905,7 @@ def main(argv: list[str] | None = None) -> int:
cfg_dir = env_dir("SKEL_CFG", "cfg")
tro_path = _in_results(args.tro, results)
hw_path = _in_results(args.hw, results)
rules_path = Path(args.rules) if args.rules else cfg_dir / RULES_FILE
rules_path = _in_cfg(args.rules, cfg_dir) if args.rules else cfg_dir / RULES_FILE
overrides_path = Path(args.overrides) if args.overrides else cfg_dir / OVERRIDES_FILE
for label, path in (("TRO-JSON", tro_path), ("Hardware-JSON", hw_path),
("Regeln", rules_path)):
@@ -955,7 +996,8 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
help="Hardware-JSON <projekt>_hw.json in %%SKEL_RESULTS%% "
"(oder ein Pfad)")
parser.add_argument("--rules", metavar="DATEI",
help=f"Regeldatei. Standard: %%SKEL_CFG%%\\{RULES_FILE}")
help=f"Regeldatei (Name in %%SKEL_CFG%% oder ein Pfad), z. B. "
f"e_planning_dematic.ini. Standard: %%SKEL_CFG%%\\{RULES_FILE}")
parser.add_argument("--overrides", metavar="DATEI",
help=f"Handkorrekturen. Standard: %%SKEL_CFG%%\\{OVERRIDES_FILE} "
f"(fehlt sie, keine)")
+14 -3
View File
@@ -150,6 +150,13 @@ class HwSource:
geraet = self.device(tro_id, slot, role, layout)
return _bekannt(fmt.format(bmk=geraet[0])) if geraet else _offen(offen)
def conflict(self, tro_id: str, slot: str, role: str) -> bool:
"""True, wenn die Anlagen-JSON fuer die Slot-Rolle einen Widerspruch fuehrt."""
if self.anlage is None:
return False
b = self.anlage.binding(tro_id, slot, role)
return b is not None and b.status == "widerspruch"
def conveyor(self, host: str) -> ConveyorBinding | None:
"""Foerderer der Hardwareplanung (K0102) am Layout-Foerderer ("0010-R")."""
if self.anlage is None or not host:
@@ -643,7 +650,9 @@ def _drive(tro: TroBase, hw: HwSource) -> tuple[SclName, SclName]:
Erst der Antriebs-Slot (Rollen motor/motorProtection); fehlt er, der
Foerderer der Hardwareplanung am Host des TRO - der Vario-Antrieb ist
dessen Motor (MA/FC-Paar aus hw_import).
dessen Motor (MA/FC-Paar aus hw_import). Ein Widerspruch im Slot (z. B.
zwei Vario-TROs auf einem Foerderer) bleibt offen, statt den Motor des
Foerderers zu erben.
"""
motor = hw.output(tro.tro_id, "drive", "motor", offen="MA____")
schutz = hw.output(tro.tro_id, "drive", "motorProtection",
@@ -651,9 +660,11 @@ def _drive(tro: TroBase, hw: HwSource) -> tuple[SclName, SclName]:
hosts = list(getattr(tro, "hosts", []) or [])
conveyor = hw.conveyor(hosts[0]) if hosts else None
if conveyor is not None:
if motor.todo and conveyor.motor:
if (motor.todo and conveyor.motor
and not hw.conflict(tro.tro_id, "drive", "motor")):
motor = _bekannt(conveyor.motor)
if schutz.todo and conveyor.protection:
if (schutz.todo and conveyor.protection
and not hw.conflict(tro.tro_id, "drive", "motorProtection")):
schutz = _bekannt(f'"{conveyor.protection}"')
return motor, schutz
+85
View File
@@ -191,6 +191,91 @@ def test_foerderer_aus_klartext(text, name, kind):
assert conveyor_kind_of(text) == kind
@pytest.mark.parametrize("text, name, kind", [
# Dematic nennt nur den Motor
("Motor KR-MA0055", "K0055", "Kreisel"),
("Rad dreht ILS-KR (MA0046)", "K0046", "Kreisel"),
("Motor ILS-VARIO-MA0043", "K0043", "Strecke"),
("Abfrage Finger in Position ILS-VARIO (MA0050)", "K0050", "Strecke"),
# K/# gehen vor: der Motor MA0202 treibt hier K0102
("ANTRIEB KREISEL-K0102 MA0202", "K0102", "Kreisel"),
("Separator Str. 1", "", ""),
])
def test_foerderer_aus_motornummer(text, name, kind):
assert conveyor_of(text, motor=True) == name
assert conveyor_kind_of(text) == kind
# ohne motor=True (Mubea) zaehlt die Motornummer nicht
assert conveyor_of(text) == ("K0102" if "K0102" in text else "")
# ---------------------------------------------------------------------------
# Gemeinsame Zeichnung (Layout + E-Planung in einer DXF, Dematic)
# ---------------------------------------------------------------------------
def dxf_file(tmp_path: Path) -> Path:
"""E-Symbole wie in dematic_merge..dxf: Bloecke mit IO/VERW/BEZEICHNUNG."""
import ezdxf
doc = ezdxf.new()
tags = ("SPS", "IO", "VERW", "BEZEICHNUNG", "KENNZEICHNUNG", "ID")
sym = doc.blocks.new("ILS_720002003-Separator")
for i, tag in enumerate(tags):
sym.add_attdef(tag, (0, -i * 10))
label = doc.blocks.new("ILS_Motorkennzeichnung-Kreisel")
label.add_attdef("MOTORKENNZEICHNUNG", (0, 0))
msp = doc.modelspace()
def place(x, y, **attrs):
msp.add_blockref("ILS_720002003-Separator", (x, y)).add_auto_attribs(attrs)
place(100.0, 200.0, SPS="2", IO="BG0532", VERW="In Sep 1",
BEZEICHNUNG="in Separator Str. 1", KENNZEICHNUNG="=A02+UC0103-KF1DI1")
place(100.0, 200.0, SPS="2", IO="MB0533", VERW="Sep 1",
BEZEICHNUNG="Separator Str. 1", KENNZEICHNUNG="=A02+UC0103-KF1DQ1")
place(900.0, 50.0, SPS="2", IO="MA0202", VERW="ILS-KR-MA0202",
BEZEICHNUNG="Motor KR-MA0202", KENNZEICHNUNG="=A02+UH01-KF1DQ2")
# doppelt: der Eintrag mit VERW gewinnt
place(5.0, 5.0, IO="BG0777")
place(7.0, 7.0, IO="BG0777", VERW="Stau 1")
# ohne BMK in IO: kein E-Bauteil
place(0.0, 0.0, IO="")
msp.add_blockref("ILS_Motorkennzeichnung-Kreisel", (0, 0)).add_auto_attribs(
{"MOTORKENNZEICHNUNG": "MA0999"})
path = tmp_path / "anlage_merge.dxf"
doc.saveas(path)
return path
def test_zeichnung_liest_jeden_block_mit_bmk(tmp_path):
warnings: list[str] = []
devices = {d.bmk: d for d in hw_import.load_dxf(dxf_file(tmp_path), warnings)}
assert sorted(devices) == ["BG0532", "BG0777", "MA0202", "MB0533"]
s = devices["BG0532"]
assert (s.verw, s.bezeichnung, s.pos, s.station) == (
"In Sep 1", "in Separator Str. 1", (100.0, 200.0), "KF1DI1")
assert (devices["BG0777"].verw, devices["BG0777"].pos) == ("Stau 1", (7.0, 7.0))
assert warnings == ["Zeichnung: BG0777 steht 2x in der Zeichnung - verwendet wird ein Eintrag"]
def test_zeichnung_mit_tia_lage_in_layout_koordinaten(tmp_path):
doc = build_hardware(tia_file(tmp_path), None, "X", dxf_path=dxf_file(tmp_path),
conveyor_from_motor=True)
assert doc.layout_coordinates and doc.positions_file == "anlage_merge.dxf"
s = doc.by_bmk()["BG0532"]
assert s.sources == ["tia", "positions"] and s.constant == "cInBG0532"
[drive] = doc.drives
# K0102 aus dem TIA-Kommentar geht der Motornummer vor
assert (drive.motor, drive.protection, drive.conveyor) == ("MA0202", "FC0202", "K0102")
assert HardwareDocument.from_json(doc.to_json()).layout_coordinates
def test_positionsdatei_und_zeichnung_schliessen_sich_aus(tmp_path):
with pytest.raises(ValueError, match="nur eine Quelle"):
build_hardware(tia_file(tmp_path), positions_file(tmp_path), "X",
dxf_path=dxf_file(tmp_path))
# ---------------------------------------------------------------------------
# Zusammenfuehren
# ---------------------------------------------------------------------------
+35 -2
View File
@@ -37,7 +37,8 @@ def _tro(tro_id, type_name, seps, hosts=()):
separators=list(seps), scanners=[], reason="Test")
def layout() -> TroDocument:
def layout(second_vario: bool = False) -> TroDocument:
"""``second_vario``: TRO06 ist ein zweiter Vario-TRO auf VF_1 (Dematic VF_6)."""
parts = [_part(i, "Separator", x, y) for i, (x, y) in SEPS.items()]
parts += [
_part("0030", "Ausschleus", 0.0, 2000.0),
@@ -52,7 +53,8 @@ def layout() -> TroDocument:
_tro("TRO03", "Vario", ["0003"], ["0020"]),
_tro("TRO04", "1Sep", ["0004"]),
_tro("TRO05", "1Sep", ["0005"]),
_tro("TRO06", "1Sep", ["0006"]),
_tro("TRO06", "Vario", ["0006"], ["0020"]) if second_vario
else _tro("TRO06", "1Sep", ["0006"]),
]
return TroDocument.from_analysis(Analysis(tros=tros), source="x.csv", parts=parts)
@@ -183,6 +185,37 @@ def test_vario_antrieb_aus_dem_foerderer():
assert anlage.binding("TRO03", "drive", "senJam").status == "offen"
def test_zwei_antriebs_tros_auf_einem_foerderer_sind_widerspruch():
"""Jeder Vario-TRO hat seinen eigenen Motor; die Mehrheit je Foerderer
kennt nur einen - nicht beiden denselben Motor geben."""
anlage = run(doc=layout(second_vario=True))
for tro in ("TRO03", "TRO06"):
motor = anlage.binding(tro, "drive", "motor")
assert (motor.bmk, motor.status) == ("MA0210", "widerspruch")
assert "TRO03, TRO06" in motor.note and "von Hand" in motor.note
assert anlage.binding(tro, "drive", "motorProtection").status == "widerspruch"
def test_gemeinsame_zeichnung_ohne_lagebezug(tmp_path):
"""hw_import --dxf: E-Symbole stehen schon in Layout-Koordinaten - kein
Lagebezug wird ermittelt oder abgelegt."""
hw = hardware()
hw.layout_coordinates = True
anlage = run(hw=hw, transform=None, tmp_path=tmp_path)
reg = anlage.registration
assert (reg.dx, reg.dy, reg.rotation, reg.source) == (0.0, 0.0, 0.0, "zeichnung")
assert not list(tmp_path.iterdir())
assert anlage.binding("TRO01", "separator1", "sensor").bmk == "BG0101"
def test_dematic_regeln_gleiche_nummer():
rules, _ = load_rules(RULES.parent / "e_planning_dematic.ini")
by_role = {r.role: r for r in rules}
assert by_role["Separator.stopper"].number_from == "Separator.sensor"
assert by_role["Separator.stopper"].number_offset == 0
assert by_role["Ausschleus.valve"].number_offset == 0
def test_foerderer_mit_zu_wenig_stimmen_bleibt_offen():
hw = hardware()
hw.devices = [d for d in hw.devices if d.bmk != "BG0802"]
+14
View File
@@ -120,6 +120,20 @@ def test_vario_antrieb_ueber_den_foerderer():
assert tro.vario_sensors["stSenFinger"].todo
def test_widerspruch_im_antrieb_erbt_nicht_den_foerderermotor():
"""Zwei Vario-TROs auf einem Foerderer: merge_hw_layout fuehrt den Antrieb
als Widerspruch - dann nicht doch den Motor des Foerderers einsetzen."""
doc = anlage()
doc.bindings += [
_bind("TRO03", "drive", "motor", "MA0210", "cOutMA0210", status="widerspruch"),
_bind("TRO03", "drive", "motorProtection", "FC0210", "cInFC0210",
status="widerspruch"),
]
tro = _by_id(_kontext(doc), "TRO03")
assert (tro.vario_motor.value, tro.vario_motor.todo) == ("MA____", True)
assert tro.motor_protection.todo
def test_ohne_hardwareplanung_alles_offen():
doc = anlage()
ctx = build_context(doc.layout, "anlage_tro.json")