# -*- coding: utf-8 -*- """Tests fuer lib/hw_import.py - TIA-Import + Positionsdatei -> HardwareDocument. Die TIA-Datei wird hier als kleine xlsx gebaut (zipfile, wie sie der TIA-Export schreibt: Blatt "PLC Tags" und "Constants"); Namen, Adressen und Klartexte nach dem Mubea-Datensatz (data/Mubea/mubea-1_TIA.xlsx). """ import json import zipfile from pathlib import Path import pytest import hw_import from hw_import import ( HardwareDocument, build_hardware, conveyor_kind_of, conveyor_of, load_tia, project_name, read_xlsx, ) _HEAD = ('' '<{tag} xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ' 'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">') def _col(i: int) -> str: out = "" i += 1 while i: i, r = divmod(i - 1, 26) out = chr(65 + r) + out return out def write_xlsx(path: Path, sheets: dict[str, list[list]], shared: bool = True) -> Path: """Minimale xlsx: Texte als Shared Strings (oder inline), Zahlen als Werte.""" strings: list[str] = [] files = {} for n, (name, rows) in enumerate(sheets.items(), start=1): xml_rows = [] for r, row in enumerate(rows, start=1): cells = [] for c, value in enumerate(row): ref = f"{_col(c)}{r}" if value is None: continue if isinstance(value, (int, float)): cells.append(f'{value}') elif shared: if value not in strings: strings.append(value) cells.append(f'{strings.index(value)}') else: cells.append(f'{value}') xml_rows.append(f'{"".join(cells)}') files[f"xl/worksheets/sheet{n}.xml"] = ( _HEAD.format(tag="worksheet") + f'{"".join(xml_rows)}') sheet_xml = "".join(f'' for n, name in enumerate(sheets, start=1)) files["xl/workbook.xml"] = _HEAD.format(tag="workbook") + f"{sheet_xml}" rels = "".join(f'' for n in range(1, len(sheets) + 1)) files["xl/_rels/workbook.xml.rels"] = ( '' f"{rels}") if shared and strings: si = "".join(f"{s}" for s in strings) files["xl/sharedStrings.xml"] = _HEAD.format(tag="sst") + f"{si}" with zipfile.ZipFile(path, "w") as z: for name, text in files.items(): z.writestr(name, text) return path TAG_HEADER = ["Name", "Path", "Data Type", "Logical Address", "Comment", "Hmi Visible"] CONST_HEADER = ["Name", "Path", "Data Type", "Value", "Comment"] def tia_file(tmp_path: Path, extra_tags=(), extra_consts=()) -> Path: tags = [ ["BG0532", "Import", "Bool", "%E053.2", "SENSOR IN SEP KR-K0102", "True"], ["MB0533", "Import", "Bool", "%A053.3", "STOPPER KR-K0102", "True"], ["MA0202", "Import", "Bool", "%A020.2", "ANTRIEB KREISEL-K0102", "True"], ["FC0202", "Import", "Bool", "%E020.2", "MOTORSCHUTZ KREISEL-K0102", "True"], ["GF20000", "Import", "Bool", "%A2000.0", "INKREMENTALGEBER KREISEL K0102 (M0205)", "True"], ["SF0001", "Import", "Bool", "%E000.1", "ANLAGE EIN", "True"], ["BG0999", "Import", "Bool", "%E099.0", "NUR IM TIA", "True"], *extra_tags, ] consts = [ ["cInBG0532", "Import", "Int", 0], ["cOutMB0533", "Import", "Int", 0], ["cOutMA0202", "Import", "Int", 1], ["cInFC0202", "Import", "Int", 1], ["cOutGF20000", "Import", "Int", 2], ["cInSF0001", "Import", "Int", 2], ["cInBG0999", "Import", "Int", 3], *extra_consts, ] return write_xlsx(tmp_path / "x_TIA.xlsx", {"PLC Tags": [TAG_HEADER, *tags], "Constants": [CONST_HEADER, *consts]}) def positions_file(tmp_path: Path) -> Path: data = { "sensors": { "BG0532@1": {"IO": "BG0532", "VERW": "IN SEP", "BEZEICHNUNG": "SENSOR IN SEP KR-K0102", "KENNZEICHNUNG": "=A1+UH01-KF102", "pos": [100.0, 200.0]}, "MB0533@1": {"IO": "MB0533", "VERW": "SEP", "BEZEICHNUNG": "STOPPER KR-K0102", "KENNZEICHNUNG": "=A1+UH01-KF102", "pos": [100.0, 470.0]}, "BG0514@1": {"IO": "BG0514", "VERW": "KETTENBRUCH", "BEZEICHNUNG": "SENSOR KETTENBRUCH CONVEYOR #101", "KENNZEICHNUNG": "", "pos": [5.0, 5.0]}, }, "schaltschrank_elemente": { "MA0202@1": {"IO": "MA0202", "VERW": "MOT", "BEZEICHNUNG": "ANTRIEB KREISEL-K0102", "KENNZEICHNUNG": "=A1+UH01-KF1DQ3"}, "FC0202@1": {"IO": "FC0202", "BEZEICHNUNG": "MOTORSCHUTZ KREISEL-K0102", "KENNZEICHNUNG": "=A1+UH01-KF1DI2"}, "QA7000@A/B": {"IO": "QA7000", "VERW": "IL-YE-LED", "BEZEICHNUNG": "HAUPTSCHUETZE", "KENNZEICHNUNG": "=A1+UH01-KF1FDQ1"}, "SF0001@1": {"IO": "SF0001", "BEZEICHNUNG": "ANLAGE EIN"}, "GF20000@1": {"IO": "GF20000", "BEZEICHNUNG": "INKREMENTALGEBER KREISEL K0102 (M0205)"}, }, "warnings": {"mapping_warnings": {"BG0514@1": "Ungueltiger Pfad in Kennzeichnung: "}}, } path = tmp_path / "Xproj_positionsconv.json" path.write_text(json.dumps(data), encoding="utf-8") return path # --------------------------------------------------------------------------- # xlsx und TIA-Import # --------------------------------------------------------------------------- @pytest.mark.parametrize("shared", [True, False]) def test_xlsx_lesen(tmp_path, shared): path = write_xlsx(tmp_path / "t.xlsx", {"A": [["Name", "Wert"], ["x", 3], [None, "y"]]}, shared=shared) assert read_xlsx(path) == {"A": [["Name", "Wert"], ["x", "3"], ["", "y"]]} def test_tia_blaetter_an_der_kopfzeile_erkannt(tmp_path): """Blattnamen sind egal (deutscher Export), gezaehlt wird die Kopfzeile.""" path = write_xlsx(tmp_path / "de_TIA.xlsx", { "PLC-Variablen": [["Name", "Pfad", "Datentyp", "Logische Adresse", "Kommentar"], ["BG0532", "Import", "Bool", "%I53.2", "SENSOR"]], "Konstanten": [["Name", "Pfad", "Datentyp", "Wert", "Kommentar"], ["cInBG0532", "Import", "Int", "7", ""]], "Leer": [["irgendwas"]], }) warnings: list[str] = [] tags, consts = load_tia(path, warnings) assert [(t.name, t.address, t.comment) for t in tags] == [("BG0532", "%I53.2", "SENSOR")] assert [(c.name, c.value) for c in consts] == [("cInBG0532", "7")] assert any("'Leer'" in w for w in warnings) def test_tia_ohne_tags_ist_ein_fehler(tmp_path): path = write_xlsx(tmp_path / "x.xlsx", {"Constants": [CONST_HEADER, ["cInBG1", "", "Int", 0]]}) with pytest.raises(ValueError, match="kein Blatt mit Tags"): load_tia(path) # --------------------------------------------------------------------------- # Klartext -> Foerderer # --------------------------------------------------------------------------- @pytest.mark.parametrize("text, name, kind", [ ("ANTRIEB KREISEL-K0102", "K0102", "Kreisel"), ("PRUEFUNG BT. POS.1 (KR-K0102 EINFAHRT PUFFER)", "K0102", "Kreisel"), # dieselbe Strecke einmal als K0103, einmal als #103 ("MOTORSCHUTZ CONVEYOR-K0103", "K0103", "Strecke"), ("MOTOR TEF LBT CONVEYOR-#103", "K0103", "Strecke"), ("PIN CONVEYOR #109(EXPRESS)", "K0109", "Strecke"), # FC0204 im Text ist kein Foerderer ("MOTORSCHUTZ FC0204 CONVEYOR #110", "K0110", "Strecke"), ("SEPARATOR STR. 14", "", ""), ]) def test_foerderer_aus_klartext(text, name, kind): assert conveyor_of(text) == name assert conveyor_kind_of(text) == kind # --------------------------------------------------------------------------- # Zusammenfuehren # --------------------------------------------------------------------------- def test_beide_quellen_je_bmk_zusammengefuehrt(tmp_path): doc = build_hardware(tia_file(tmp_path), positions_file(tmp_path), "Xproj") dev = doc.by_bmk() s = dev["BG0532"] assert (s.direction, s.address, s.constant, s.constant_value) == ("in", "%E053.2", "cInBG0532", 0) assert (s.verw, s.station, s.controller, s.pos) == ("IN SEP", "KF102", "UH01", (100.0, 200.0)) assert s.sources == ["tia", "positions"] assert (s.conveyor, s.conveyor_kind) == ("K0102", "Kreisel") assert dev["MB0533"].constant == "cOutMB0533" and dev["MB0533"].direction == "out" # nur in einer Quelle assert dev["QA7000"].sources == ["positions"] and dev["QA7000"].constant == "" assert dev["BG0999"].sources == ["tia"] and dev["BG0999"].pos is None assert [d.bmk for d in doc.field_devices()] == ["BG0514", "BG0532", "MB0533"] assert doc.tia_sha256 and doc.positions_sha256 assert doc.project == "Xproj" def test_antrieb_mit_motorschutz_und_geber(tmp_path): doc = build_hardware(tia_file(tmp_path), positions_file(tmp_path)) [drive] = doc.drives assert (drive.motor, drive.protection, drive.conveyor, drive.conveyor_kind, drive.encoder) == ( "MA0202", "FC0202", "K0102", "Kreisel", "GF20000") def test_gegenpruefung(tmp_path): doc = build_hardware( tia_file(tmp_path, extra_tags=[["BG0533", "Import", "Bool", "%E053.2", "GLEICHE ADRESSE", ""]], extra_consts=[["cInBG4711", "Import", "Int", 9]]), positions_file(tmp_path)) checks = "\n".join(doc.checks) assert "nur in der Positionsdatei, nicht im TIA-Import (2): BG0514, QA7000" in checks assert "Feldgeraet nur im TIA-Import, nicht in der Positionsdatei (2): BG0533, BG0999" in checks assert "Signal ohne Index-Konstante cIn/cOut (1): BG0533" in checks assert "Index-Konstante ohne Signal (1): cInBG4711" in checks assert "Adresse mehrfach vergeben: %E053.2" in checks assert "Feldgeraet ohne Steuerung in KENNZEICHNUNG (1): BG0514" in checks assert any("BG0514" in w and "mapping_warnings" in w for w in doc.warnings) assert [c.name for c in doc.constants] == ["cInBG4711"] def test_nur_tia_ohne_positionsdatei(tmp_path): doc = build_hardware(tia_file(tmp_path), None) assert doc.positions_file == "" and doc.field_devices() == [] assert all(d.sources == ["tia"] for d in doc.devices) def test_json_rundreise(tmp_path): doc = build_hardware(tia_file(tmp_path), positions_file(tmp_path), "Xproj") again = HardwareDocument.from_json(doc.to_json()) assert again == doc assert json.loads(doc.to_json())["schema"] == "sps_skel-hw-v1" def test_projektname(): assert project_name(None, Path("Mubea_positionsconv.json")) == "Mubea" assert project_name(None, Path("Mubea_positionsdraw.json")) == "Mubea" assert project_name(Path("mubea-1_TIA.xlsx"), None) == "mubea-1" def test_cli_mit_unterordner_in_skel_data(tmp_path, monkeypatch, capsys): """--tia Mubea/x_TIA.xlsx wird ab SKEL_DATA gefunden (Befund der Nachpruefung).""" data = tmp_path / "data" / "Xproj" data.mkdir(parents=True) tia_file(data) positions_file(data) monkeypatch.setenv("SKEL_DATA", str(tmp_path / "data")) monkeypatch.setenv("SKEL_RESULTS", str(tmp_path / "results")) monkeypatch.chdir(tmp_path) rc = hw_import.main(["--tia", "Xproj/x_TIA.xlsx", "--positions", "Xproj/Xproj_positionsconv.json"]) assert rc == 0 written = HardwareDocument.from_json( (tmp_path / "results" / "Xproj_hw.json").read_text(encoding="utf-8")) assert written.project == "Xproj" and len(written.devices) == 9 assert (tmp_path / "results" / "Xproj_hw.md").is_file() assert "HARDWAREPLANUNG EINLESEN" in capsys.readouterr().out