ParamManagerScripts/backend/script_groups/IO_adaptation/x1_export_CAx.py

372 lines
14 KiB
Python

"""
export_CAx_from_tia : Script que exporta los datos CAx de un proyecto de TIA Portal y genera un resumen en Markdown.
"""
import tkinter as tk
from tkinter import filedialog
import os
import sys
import traceback
import xml.etree.ElementTree as ET # Library to parse XML (AML)
from pathlib import Path # Import Path
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration
# --- Configuration ---
# Supported TIA Portal versions mapping (extension -> version)
SUPPORTED_TIA_VERSIONS = {
".ap18": "18.0",
".ap19": "19.0",
".ap20": "20.0"
}
# --- TIA Scripting Import Handling ---
# (Same import handling as the previous script)
if os.getenv("TIA_SCRIPTING"):
sys.path.append(os.getenv("TIA_SCRIPTING"))
else:
pass
try:
import siemens_tia_scripting as ts
except ImportError:
print("ERROR: Failed to import 'siemens_tia_scripting'.")
print("Ensure TIA Openness, the module, and Python 3.12.X are set up.")
sys.exit(1)
except Exception as e:
print(f"An unexpected error occurred during import: {e}")
traceback.print_exc()
sys.exit(1)
# --- Functions ---
def get_supported_filetypes():
"""Returns the supported file types for TIA Portal projects."""
filetypes = []
for ext, version in SUPPORTED_TIA_VERSIONS.items():
version_major = version.split('.')[0]
filetypes.append((f"TIA Portal V{version_major} Projects", f"*{ext}"))
# Add option to show all supported files
all_extensions = " ".join([f"*{ext}" for ext in SUPPORTED_TIA_VERSIONS.keys()])
filetypes.insert(0, ("All TIA Portal Projects", all_extensions))
return filetypes
def detect_tia_version(project_file_path):
"""Detects TIA Portal version based on file extension."""
file_path = Path(project_file_path)
file_extension = file_path.suffix.lower()
if file_extension in SUPPORTED_TIA_VERSIONS:
detected_version = SUPPORTED_TIA_VERSIONS[file_extension]
print(f"Detected TIA Portal version: {detected_version} (from extension {file_extension})")
return detected_version
else:
print(f"WARNING: Unrecognized file extension '{file_extension}'. Supported extensions: {list(SUPPORTED_TIA_VERSIONS.keys())}")
# Default to version 18.0 for backward compatibility
print("Defaulting to TIA Portal V18.0")
return "18.0"
def select_project_file():
"""Opens a dialog to select a TIA Portal project file."""
root = tk.Tk()
root.withdraw()
file_path = filedialog.askopenfilename(
title="Select TIA Portal Project File",
filetypes=get_supported_filetypes(),
)
root.destroy()
if not file_path:
print("No project file selected. Exiting.")
sys.exit(0)
return file_path
def select_output_directory():
"""Opens a dialog to select the output directory."""
root = tk.Tk()
root.withdraw()
dir_path = filedialog.askdirectory(
title="Select Output Directory for AML and MD files"
)
root.destroy()
if not dir_path:
print("No output directory selected. Exiting.")
sys.exit(0)
return dir_path
def find_elements(element, path):
"""Helper to find elements using namespaces commonly found in AML."""
# AutomationML namespaces often vary slightly or might be default
# This basic approach tries common prefixes or no prefix
namespaces = {
"": (
element.tag.split("}")[0][1:] if "}" in element.tag else ""
), # Default namespace if present
"caex": "http://www.dke.de/CAEX", # Common CAEX namespace
# Add other potential namespaces if needed based on file inspection
}
# Try finding with common prefixes or the default namespace
for prefix, uri in namespaces.items():
# Construct path with namespace URI if prefix is defined
namespaced_path = path
if prefix:
parts = path.split("/")
namespaced_parts = [
f"{{{uri}}}{part}" if part != "." else part for part in parts
]
namespaced_path = "/".join(namespaced_parts)
# Try findall with the constructed path
found = element.findall(namespaced_path)
if found:
return found # Return first successful find
# Fallback: try finding without explicit namespace (might work if default ns is used throughout)
# This might require adjusting the path string itself depending on the XML structure
try:
# Simple attempt without namespace handling if the above fails
return element.findall(path)
except (
SyntaxError
): # Handle potential errors if path isn't valid without namespaces
return []
def parse_aml_to_markdown(aml_file_path, md_file_path):
"""Parses the AML file and generates a Markdown summary."""
print(f"Parsing AML file: {aml_file_path}")
try:
tree = ET.parse(aml_file_path)
root = tree.getroot()
markdown_lines = ["# Project CAx Data Summary (AutomationML)", ""]
# Find InstanceHierarchy - usually contains the project structure
# Note: Namespace handling in ElementTree can be tricky. Adjust '{...}' part if needed.
# We will use a helper function 'find_elements' to try common patterns
instance_hierarchies = find_elements(
root, ".//InstanceHierarchy"
) # Common CAEX tag
if not instance_hierarchies:
markdown_lines.append("Could not find InstanceHierarchy in the AML file.")
print("Warning: Could not find InstanceHierarchy element.")
else:
# Assuming the first InstanceHierarchy is the main one
ih = instance_hierarchies[0]
markdown_lines.append(f"## Instance Hierarchy: {ih.get('Name', 'N/A')}")
markdown_lines.append("")
# Look for InternalElements which represent devices/components
internal_elements = find_elements(
ih, ".//InternalElement"
) # Common CAEX tag
if not internal_elements:
markdown_lines.append(
"No devices (InternalElement) found in InstanceHierarchy."
)
print("Info: No InternalElement tags found under InstanceHierarchy.")
else:
markdown_lines.append(
f"Found {len(internal_elements)} device(s)/component(s):"
)
markdown_lines.append("")
markdown_lines.append(
"| Name | SystemUnitClass | RefBaseSystemUnitPath | Attributes |"
)
markdown_lines.append("|---|---|---|---|")
for elem in internal_elements:
name = elem.get("Name", "N/A")
ref_path = elem.get(
"RefBaseSystemUnitPath", "N/A"
) # Path to class definition
# Try to get the class name from the RefBaseSystemUnitPath or SystemUnitClassLib
su_class_path = find_elements(
elem, ".//SystemUnitClass"
) # Check direct child first
su_class = (
su_class_path[0].get("Path", "N/A")
if su_class_path
else ref_path.split("/")[-1]
) # Fallback to last part of path
attributes_md = ""
attributes = find_elements(elem, ".//Attribute") # Find attributes
attr_list = []
for attr in attributes:
attr_name = attr.get("Name", "")
attr_value_elem = find_elements(
attr, ".//Value"
) # Get Value element
attr_value = (
attr_value_elem[0].text
if attr_value_elem and attr_value_elem[0].text
else "N/A"
)
# Look for potential IP addresses (common attribute names)
if "Address" in attr_name or "IP" in attr_name:
attr_list.append(f"**{attr_name}**: {attr_value}")
else:
attr_list.append(f"{attr_name}: {attr_value}")
attributes_md = "<br>".join(attr_list) if attr_list else "None"
markdown_lines.append(
f"| {name} | {su_class} | `{ref_path}` | {attributes_md} |"
)
# Write to Markdown file
with open(md_file_path, "w", encoding="utf-8") as f:
f.write("\n".join(markdown_lines))
print(f"Markdown summary written to: {md_file_path}")
except ET.ParseError as xml_err:
print(f"ERROR parsing XML file {aml_file_path}: {xml_err}")
with open(md_file_path, "w", encoding="utf-8") as f:
f.write(
f"# Error\n\nFailed to parse AML file: {os.path.basename(aml_file_path)}\n\nError: {xml_err}"
)
except Exception as e:
print(f"ERROR processing AML file {aml_file_path}: {e}")
traceback.print_exc()
with open(md_file_path, "w", encoding="utf-8") as f:
f.write(
f"# Error\n\nAn unexpected error occurred while processing AML file: {os.path.basename(aml_file_path)}\n\nError: {e}"
)
# --- Main Script ---
if __name__ == "__main__":
configs = load_configuration()
working_directory = configs.get("working_directory")
print("--- TIA Portal Project CAx Exporter and Analyzer ---")
# Validate working directory
if not working_directory or not os.path.isdir(working_directory):
print("ERROR: Working directory not set or invalid in configuration.")
print("Please configure the working directory using the main application.")
sys.exit(1)
# 1. Select Project File, Output Directory comes from config
project_file = select_project_file()
output_dir = Path(
working_directory
) # Use working directory from config, ensure it's a Path object
print(f"\nSelected Project: {project_file}")
print(f"Using Output Directory (Working Directory): {output_dir}")
# 2. Detect TIA Portal version from project file
tia_version = detect_tia_version(project_file)
# Define output file names using Path object
project_path = Path(project_file)
project_base_name = project_path.stem # Get filename without extension
aml_file = output_dir / f"{project_base_name}_CAx_Export.aml"
md_file = output_dir / f"{project_base_name}_CAx_Summary.md"
log_file = (
output_dir / f"{project_base_name}_CAx_Export.log"
) # Log file for the export process
print(f"Will export CAx data to: {aml_file}")
print(f"Will generate summary to: {md_file}")
print(f"Export log file: {log_file}")
portal_instance = None
project_object = None
cax_export_successful = False
try:
# 3. Connect to TIA Portal with detected version
print(f"\nConnecting to TIA Portal V{tia_version}...")
portal_instance = ts.open_portal(
version=tia_version,
portal_mode=ts.Enums.PortalMode.WithGraphicalUserInterface,
)
print("Connected.")
# 4. Open Project
print(
f"Opening project: {project_path.name}..."
) # Use Path object's name attribute
project_object = portal_instance.open_project(
project_file_path=str(project_path)
) # Pass path as string
if project_object is None:
print("Project might already be open, attempting to get handle...")
project_object = portal_instance.get_project()
if project_object is None:
raise Exception("Failed to open or get the specified project.")
print("Project opened.")
# 5. Export CAx Data (Project Level)
print(f"Exporting CAx data for the project to {aml_file}...")
# Ensure output directory exists (Path.mkdir handles this implicitly if needed later, but good practice)
output_dir.mkdir(parents=True, exist_ok=True)
# Pass paths as strings to the TIA function
export_result = project_object.export_cax_data(
export_file_path=str(aml_file), log_file_path=str(log_file)
)
if export_result:
print("CAx data exported successfully.")
cax_export_successful = True
else:
print("CAx data export failed. Check the log file for details:")
print(f" Log file: {log_file}")
# Write basic error message to MD file if export fails
with open(md_file, "w", encoding="utf-8") as f:
f.write(
f"# Error\n\nCAx data export failed. Check log file: {log_file}"
)
except ts.TiaException as tia_ex:
print(f"\nTIA Portal Openness Error: {tia_ex}")
traceback.print_exc()
except FileNotFoundError:
print(f"\nERROR: Project file not found at {project_file}")
except Exception as e:
print(f"\nAn unexpected error occurred during TIA interaction: {e}")
traceback.print_exc()
finally:
# Close TIA Portal before processing the file (or detach)
if portal_instance:
try:
print("\nClosing TIA Portal...")
portal_instance.close_portal()
print("TIA Portal closed.")
except Exception as close_ex:
print(f"Error during TIA Portal cleanup: {close_ex}")
# 6. Parse AML and Generate Markdown (only if export was successful)
if cax_export_successful:
if aml_file.exists(): # Use Path object's exists() method
parse_aml_to_markdown(aml_file, md_file)
else:
print(
f"ERROR: Export was reported successful, but AML file not found at {aml_file}"
)
with open(md_file, "w", encoding="utf-8") as f:
f.write(
f"# Error\n\nExport was reported successful, but AML file not found:\n{aml_file}"
)
print("\nScript finished.")