From 9b9d3a8f7c6d3e44f86b75ead082f57fb46585f7 Mon Sep 17 00:00:00 2001 From: mistangl Date: Thu, 18 Dec 2025 16:34:04 +0100 Subject: [PATCH] ErrorCollector standarisiert. Warnings werden von getpositions zur routing zu draw durchgereicht. --- lib/drawdxf.py | 22 +++++++- lib/error_collector.py | 2 +- lib/getpositions.py | 46 ++++++++++++++++- lib/ioconverter.py | 112 +++++++++++++++++++++++++++++++++++++---- lib/portalexport.py | 67 +++++++++++++++++++----- lib/routing.py | 14 ++++-- 6 files changed, 235 insertions(+), 28 deletions(-) diff --git a/lib/drawdxf.py b/lib/drawdxf.py index 2f19413..14a50d1 100644 --- a/lib/drawdxf.py +++ b/lib/drawdxf.py @@ -385,6 +385,12 @@ def model_from_json(json_file): if error_fields['errors_missing_attributes']: warnings_dict['missing_attributes'] = error_fields['errors_missing_attributes'] + # Warnungen aus getpositions und routing übernehmen + if 'warnings' in data: + warnings_from_upstream = data.pop('warnings') + if isinstance(warnings_from_upstream, dict): + warnings_dict.update(warnings_from_upstream) + if errors_dict: error_collector.add_errors(errors_dict) if warnings_dict: @@ -697,6 +703,20 @@ def _create_error_sheets(wb, error_collector: ErrorCollector = None): for sname, err_msg in missing_attributes.items(): ws.append([sname, err_msg]) + # Sheet: WARNINGS (für allgemeine Warnungen wie z-Koordinaten-Abweichungen) + general_warnings = {k: v for k, v in warnings.items() if k != 'missing_attributes'} + + if general_warnings: + ws = wb.create_sheet("WARNINGS") + ws.append(["Warning Type", "Details"]) + ws.column_dimensions['A'].width = 30 + ws.column_dimensions['B'].width = 80 + + for warning_type, warning_msg in general_warnings.items(): + # Formatiere den Warning Type lesbarer + readable_type = warning_type.replace('_', ' ').title() + ws.append([readable_type, str(warning_msg)]) + def _create_bom_workbook(outpath, processed_data, bezeichner_cfg): """ Erstellt eine separate Excel-Arbeitsmappe für die Stückliste (BOM) und speichert sie ab. @@ -927,7 +947,7 @@ if __name__ == '__main__': parser.add_argument('-d', '--dxf', action='store', help='this dxf drawing will be copied and the new layer with the cables will be added. Original file must be added with --origin', metavar='myfile.dxf') parser.add_argument('-n', '--new', action='store', help='create a new dxf file only with cables in it. Name is basename and a timestamp') parser.add_argument('-x', '--excel', action='store', help='create a xlsx file with cables data', metavar='allCables.xls') - parser.add_argument('-o', '--origin', action='store', help='name of original .dxf file used by -d and -a', metavar='original.dxf') + parser.add_argument('-o', '--origin', action='store', help='name of original .dxf file used by -d', metavar='original.dxf') parser.add_argument('-l', '--local', action='store_true', help='using only local data for naming of article numbers. If not set: fetching names from SIVAS.') args = parser.parse_args() diff --git a/lib/error_collector.py b/lib/error_collector.py index 1aff6fd..f7e9473 100644 --- a/lib/error_collector.py +++ b/lib/error_collector.py @@ -62,7 +62,7 @@ class ErrorCollector: all_issues = self.get_all_issues() write_json_file(all_issues, work_dir, filename) else: - print("Keine Fehler oder Warnungen gefunden. Keine Error-Datei geschrieben.") + print("Keine Fehler gefunden. Keine Error-Datei geschrieben.") def write_json_file(data: dict, out_dir: Path, filename: str) -> None: diff --git a/lib/getpositions.py b/lib/getpositions.py index 68a7bd1..51037e7 100644 --- a/lib/getpositions.py +++ b/lib/getpositions.py @@ -762,6 +762,47 @@ def check_file_in_work(work_dir: Path, filename: Path) -> tuple[Path, bool]: mypath = filename return mypath, fexists +def check_rack_z_coordinates(res_racks: dict, error_collector, config) -> None: + """ + Prüft die z-Koordinaten aller Racks und gibt eine Warnung aus, + wenn die Differenz zwischen min und max größer als der konfigurierte Schwellwert ist. + + Args: + res_racks: Dictionary mit Rack-Daten (key: rack_name, value: list of coordinates) + error_collector: ErrorCollector Instanz zum Hinzufügen von Warnungen + config: ConfigParser Objekt mit allgemein.cfg + """ + if not res_racks: + return + + # Lese Schwellwert aus Config (Standard: 2000.0 mm) + max_height_diff = config.getfloat("Racks", "MaximalTotalHeightDifferences", fallback=2000.0) + + all_z_coords = [] + + # Sammle alle z-Koordinaten aus allen Racks + for rack_name, coordinates in res_racks.items(): + if isinstance(coordinates, list): + for coord in coordinates: + if isinstance(coord, (list, tuple)) and len(coord) >= 3: + all_z_coords.append(coord[2]) + elif isinstance(coord, dict) and 'z' in coord: + all_z_coords.append(coord['z']) + + if not all_z_coords: + return + + # Berechne Min und Max + min_z = min(all_z_coords) + max_z = max(all_z_coords) + diff = max_z - min_z + + # Prüfe, ob Differenz > Schwellwert + if diff > max_height_diff: + warning_msg = f"The z-coordinates of the racks differ strong between each other from {min_z:.1f} to {max_z:.1f}" + error_collector.add_warnings({"rack_z_coordinate_deviation": warning_msg}) + #print(f"WARNING: {warning_msg}") + def check_existance(res_mappings: dict, res_dist: dict, res_pos: dict, res_tunnel: dict) -> dict: ret = dict() @@ -960,9 +1001,12 @@ if __name__ == '__main__': res_rac = get_rack_positions_iter(dxf_path) else: res_rac = get_rack_positions(msp) - + output_results['racks'] = res_rac + # Prüfe z-Koordinaten der Racks auf starke Abweichungen + check_rack_z_coordinates(res_rac, error_collector, config) + if args.console: print(to_json(res_rac)) diff --git a/lib/ioconverter.py b/lib/ioconverter.py index 5b98690..f215f7c 100644 --- a/lib/ioconverter.py +++ b/lib/ioconverter.py @@ -383,19 +383,33 @@ class ExcelConverter: else: raise ValueError("Unbekannter Export-Typ") - def export_excel(self, data, export_type, export_path): + def export_excel(self, data, export_type, export_path, getpos_errors=None, getpos_warnings=None): """ Wrapper, der aufgerufen wird und in sich die Aufbereitung der Daten und das Abspeichern als Excel abruft. + + Args: + data: Die zu exportierenden Daten. + export_type (str): Art des Exports (EA, TIA, WSCAD). + export_path (str): Pfad zur Ausgabedatei. + getpos_errors (dict, optional): Fehler-Dictionary aus getpositions.py. + getpos_warnings (dict, optional): Warnungen-Dictionary aus getpositions.py. """ # Daten vorbereiten dataframes = self._prep_data(data, export_type) - - # Excel Datei schreiben und speichern - self._write_excel(dataframes, export_type, export_path) - def _write_excel(self, dataframes, export_type, export_path): + # Excel Datei schreiben und speichern + self._write_excel(dataframes, export_type, export_path, getpos_errors, getpos_warnings) + + def _write_excel(self, dataframes, export_type, export_path, getpos_errors=None, getpos_warnings=None): """ Erzeugung der Excel-Datei im gewünschten Muster + + Args: + dataframes: Die vorbereiteten DataFrames für den Export. + export_type (str): Art des Exports (EA, TIA, WSCAD). + export_path (str): Pfad zur Ausgabedatei. + getpos_errors (dict, optional): Fehler-Dictionary aus getpositions.py. + getpos_warnings (dict, optional): Warnungen-Dictionary aus getpositions.py. """ if export_type == "EA": # Format für Simpel-Ausgabe @@ -406,26 +420,32 @@ class ExcelConverter: start_row = 0 df_eingaenge.to_excel(writer, sheet_name='Ein-, Ausgaenge', index=False, startrow=start_row) start_row += len(df_eingaenge) + 2 # Leerzeile dazwischen - + # Ausgaenge direkt drunter schreiben (ohne Header) df_ausgaenge.to_excel(writer, sheet_name='Ein-, Ausgaenge', index=False, startrow=start_row, header=False) - + # Duplikate und Errors self._write_duplicates_errors(writer, df_gleiche_eingaenge, df_gleiche_ausgaenge, df_fehler) + # Fehler und Warnungen aus getpositions.py + self._write_getpositions_errors(writer, getpos_errors, getpos_warnings) + elif export_type == "TIA": df_gleiche_eingaenge, df_gleiche_ausgaenge, df_fehler, df_consts, df_tags = dataframes - + with pd.ExcelWriter(export_path, engine='openpyxl', mode='w') as writer: # 1. PLC Tags Sheet mit Header. Spalten: Name, Path, Data Type, Logical Address, Comment, Hmi Visible, Hmi Accessible, Hmi Writeable, Typeobject ID, Version ID df_tags.to_excel(writer, sheet_name='PLC Tags', index=False) - + # 2. Constants Sheet mit Header. Spalten: Name, Path, Data Type, Value, Comment df_consts.to_excel(writer, sheet_name='Constants', index=False) # Duplikate und Errors self._write_duplicates_errors(writer, df_gleiche_eingaenge, df_gleiche_ausgaenge, df_fehler) + # Fehler und Warnungen aus getpositions.py + self._write_getpositions_errors(writer, getpos_errors, getpos_warnings) + elif export_type in ["WSCAD", "WSCAD mit Bezug"]: df_gleiche_eingaenge, df_gleiche_ausgaenge, df_fehler, df_eingaenge, df_ausgaenge = dataframes @@ -470,6 +490,9 @@ class ExcelConverter: # Duplikate und Errors self._write_duplicates_errors(writer, df_gleiche_eingaenge, df_gleiche_ausgaenge, df_fehler) + # Fehler und Warnungen aus getpositions.py + self._write_getpositions_errors(writer, getpos_errors, getpos_warnings) + def _append_df(self, target_df, source_df, include_bezug=False): """ Hilfsfunktion, die Dataframes für WSCAD Export aneinander reiht und entweder Bezug inkludiert oder nicht @@ -515,6 +538,77 @@ class ExcelConverter: if not df_fehler.empty: df_fehler.to_excel(writer, sheet_name='Fehler', index=False) + def _write_getpositions_errors(self, writer, errors_dict=None, warnings_dict=None): + """ + Schreibt Fehler und Warnungen aus getpositions.py in separate Excel-Sheets. + Diese Methode erstellt ähnliche Sheets wie in drawdxf.py für den getexdraw-Workflow. + + Args: + writer (pd.ExcelWriter): Der Excel-Writer für die Ausgabedatei. + errors_dict (dict, optional): Dictionary mit Fehlern aus getpositions. + warnings_dict (dict, optional): Dictionary mit Warnungen aus getpositions. + """ + if not errors_dict and not warnings_dict: + return + + # Sheet: ERR-Equipment-Connection (nur für ioconverter relevant) + if errors_dict: + errors_sensors = errors_dict.get('sensors_connection', []) + errors_dists = errors_dict.get('dists_connection', []) + + if errors_sensors or errors_dists: + data = [] + for error in errors_sensors: + data.append({ + "Type": "Sensor / Actuator", + "ID": error['name'], + "x": int(error['coords']['x']), + "y": int(error['coords']['y']) + }) + for error in errors_dists: + data.append({ + "Type": "Subdistributor", + "ID": error['name'], + "x": int(error['coords']['x']), + "y": int(error['coords']['y']) + }) + + df = pd.DataFrame(data) + df.to_excel(writer, sheet_name='ERR-Equipment-Connection', index=False) + + # Sheet: Duplicate IDs + errors_duplicates = errors_dict.get('duplicate_ids', {}) + if errors_duplicates: + data = [] + for id_name, positions in errors_duplicates.items(): + for pos in positions: + data.append({ + "ID": id_name, + "x": int(pos[0]), + "y": int(pos[1]) + }) + df = pd.DataFrame(data) + df.to_excel(writer, sheet_name='ERR-Duplicate-IDs', index=False) + + # Sheet: Tunnel Errors + errors_tunnels = errors_dict.get('overdefined_tunnel', []) + if errors_tunnels: + df = pd.DataFrame({"Tunnel ID": errors_tunnels}) + df.to_excel(writer, sheet_name='ERR-Tunnels', index=False) + + # Sheet: Warnungen (fehlende Attribute) + if warnings_dict: + missing_attributes = warnings_dict.get('missing_attributes', {}) + if missing_attributes: + data = [] + for id_name, error_msg in missing_attributes.items(): + data.append({ + "ID": id_name, + "Error Detail": error_msg + }) + df = pd.DataFrame(data) + df.to_excel(writer, sheet_name='WARN-Attributes', index=False) + def call_portal_to_excel(self, textlines, konvertierungsart): ec = ExcelConverter() diff --git a/lib/portalexport.py b/lib/portalexport.py index ecd80e8..3f638a3 100644 --- a/lib/portalexport.py +++ b/lib/portalexport.py @@ -73,16 +73,55 @@ def check_file_in_work(work_dir:Path, filename:Path): mypath = filename return (mypath, fexists) +# Helper zum Laden der Error-JSON +def load_error_json(json_path: Path, work_dir: Path): + """ + Lädt die Fehler-JSON-Datei, falls vorhanden. + + Args: + json_path (Path): Pfad zur Haupt-JSON-Datei. + work_dir (Path): Arbeitsverzeichnis. + + Returns: + tuple: (errors_dict, warnings_dict) oder (None, None) wenn keine Fehler vorhanden. + """ + # Konstruiere Error-JSON-Dateinamen basierend auf der Positions-JSON + basename = json_path.stem + if basename.endswith("_positionsconv"): + basename = basename.replace("_positionsconv", "") + elif basename.endswith("_positionsdraw"): + basename = basename.replace("_positionsdraw", "") + + error_json_name = f"{basename}_errors.json" + error_json_path = work_dir / error_json_name + + # Prüfen ob Error-JSON existiert + if not error_json_path.exists(): + # Auch im gleichen Verzeichnis wie die Positions-JSON prüfen + error_json_path = json_path.parent / error_json_name + if not error_json_path.exists(): + return None, None + + # Error-JSON laden + try: + error_data = load_json(error_json_path) + errors_dict = error_data.get("errors", None) + warnings_dict = error_data.get("warnings", None) + return errors_dict, warnings_dict + except Exception as e: + print(f"Warnung: Fehler beim Laden der Error-JSON: {e}") + return None, None + # Helper für Export -def do_export(conv_obj:ExcelConverter, mode_str, base_input, output_dir, lines): +def do_export(conv_obj:ExcelConverter, mode_str, base_input, output_dir, lines, getpos_errors=None, getpos_warnings=None): # Für WSCAD Dateinamen anpassen (Export-Typ bleibt "WSCAD" oder "WSCAD mit Bezug") - if mode_str == "WSCAD": + if mode_str == "WSCAD": if args.no_bezug: export_type = "WSCAD" - filename = f"{base_input}_WSCAD_no_Bezug.xlsx" + filename = f"{base_input}_WSCAD_no_Bezug.xlsx" else: export_type = "WSCAD mit Bezug" - filename = f"{base_input}_WSCAD.xlsx" + filename = f"{base_input}_WSCAD.xlsx" else: export_type = mode_str filename = f"{base_input}_{mode_str}.xlsx" @@ -90,19 +129,19 @@ def do_export(conv_obj:ExcelConverter, mode_str, base_input, output_dir, lines): export_path = os.path.join(output_dir, filename) try: - conv_obj.export_excel(lines, export_type, export_path) + conv_obj.export_excel(lines, export_type, export_path, getpos_errors, getpos_warnings) print(f"Export '{export_type}' erfolgreich: {export_path}") except Exception as e: print(f"Fehler bei Export '{export_type}': {e}") -def do_exports(converter, export_mode, output_dir, base_input, lines): +def do_exports(converter, export_mode, output_dir, base_input, lines, getpos_errors=None, getpos_warnings=None): if export_mode: - do_export(converter, export_mode, base_input, output_dir, lines) + do_export(converter, export_mode, base_input, output_dir, lines, getpos_errors, getpos_warnings) else: # Kein Modus → alle drei exportieren - do_export(converter, "EA", base_input, output_dir, lines) - do_export(converter, "TIA", base_input, output_dir, lines) - do_export(converter, "WSCAD", base_input, output_dir, lines) + do_export(converter, "EA", base_input, output_dir, lines, getpos_errors, getpos_warnings) + do_export(converter, "TIA", base_input, output_dir, lines, getpos_errors, getpos_warnings) + do_export(converter, "WSCAD", base_input, output_dir, lines, getpos_errors, getpos_warnings) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Excel-Konverter für BMK-Daten") @@ -176,12 +215,16 @@ if __name__ == "__main__": rawdata = load_json(json_path) (dsensors, lsensors) = prepare_data(rawdata) + + # Fehler-JSON laden (falls vorhanden) + getpos_errors, getpos_warnings = load_error_json(json_path, work_dir) + if len(dsensors) == 0: - do_exports(converter, export_mode, output_dir, basename_out, lsensors) + do_exports(converter, export_mode, output_dir, basename_out, lsensors, getpos_errors, getpos_warnings) else: for sps_nr, lines in dsensors.items(): name = f"{basename_out}-{sps_nr}" - do_exports(converter, export_mode, output_dir, name, lines) + do_exports(converter, export_mode, output_dir, name, lines, getpos_errors, getpos_warnings) else: parser.print_help() diff --git a/lib/routing.py b/lib/routing.py index 36acc25..df7917a 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -160,9 +160,13 @@ def prepare_data(rawdata:dict): errors_sensors = rawdata["not_found"]["missing_sensors"] if "missing_attributes" in rawdata["not_found"]: errors_attributes = rawdata["not_found"]["missing_attributes"] - - - return (dracks, dsensors, dsubdists, mapping, dtunnels, dtunlength, errors_dists, errors_sensors, errors_attributes) + + # Warnungen aus getpositions übernehmen + warnings = dict() + if "warnings" in rawdata: + warnings = rawdata["warnings"] + + return (dracks, dsensors, dsubdists, mapping, dtunnels, dtunlength, errors_dists, errors_sensors, errors_attributes, warnings) if __name__ == "__main__": @@ -188,7 +192,7 @@ if __name__ == "__main__": # Einlesen und Vorbereiten der Daten rawdata = load_json(sensors_path) - (racks, sensors, subdists, mapping, tunnels, tunlength, errors_dists, errors_sensors, errors_attributes) = prepare_data(rawdata) + (racks, sensors, subdists, mapping, tunnels, tunlength, errors_dists, errors_sensors, errors_attributes, warnings) = prepare_data(rawdata) config = configparser.ConfigParser(allow_no_value=True, delimiters=("=")) config.optionxform = lambda optionstr: optionstr # preserve case for letters @@ -212,6 +216,8 @@ if __name__ == "__main__": cable_paths["errors_dists_not_in_layout"] = errors_dists cable_paths["errors_sensors_not_in_layout"] = errors_sensors cable_paths["errors_missing_attributes"] = errors_attributes + # Warnungen aus getpositions übertragen + cable_paths["warnings"] = warnings write_results(to_json(cable_paths), work_dir, f"{basename}.json")