ErrorCollector standarisiert. Warnings werden von getpositions zur routing zu draw durchgereicht.

This commit is contained in:
2025-12-18 16:34:04 +01:00
parent a731889561
commit 9b9d3a8f7c
6 changed files with 235 additions and 28 deletions
+21 -1
View File
@@ -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()
+1 -1
View File
@@ -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:
+45 -1
View File
@@ -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))
+103 -9
View File
@@ -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()
+55 -12
View File
@@ -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()
+10 -4
View File
@@ -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")