350 lines
16 KiB
Python
350 lines
16 KiB
Python
import json
|
|
import os
|
|
import time
|
|
|
|
# Moved get_application_path here temporarily to avoid circular import
|
|
# Ideally, this would be passed in or the config path logic revisited.
|
|
def get_application_path():
|
|
"""Get the base path for the application, works for both dev and PyInstaller"""
|
|
import sys # Local import to avoid being at top level if utils is preferred
|
|
if getattr(sys, 'frozen', False):
|
|
return os.path.dirname(sys.executable)
|
|
else:
|
|
# In dev mode, assume config manager is in the same dir as the main script
|
|
# This might need adjustment depending on final structure
|
|
return os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
# Default configuration values
|
|
# Keep default mappings simple here, complex defaults handled in GUI if needed
|
|
DEFAULT_CONFIG = {
|
|
'file_path': '',
|
|
'project_title': 'MyScadaProject',
|
|
'parent_project': '', # Often empty for top-level projects
|
|
'view_name': 'MyView',
|
|
'view_path': '', # Optional path within views, e.g., 'Folder/Subfolder'
|
|
'svg_url': '', # Optional background image URL/Path for the view
|
|
'image_width': 800,
|
|
'image_height': 600,
|
|
'default_width': 100, # Default component width
|
|
'default_height': 50, # Default component height
|
|
'ignition_base_dir': 'C:/Program Files/Inductive Automation/Ignition/data/projects', # Added
|
|
'custom_export_dir': '', # Custom directory for SCADA export (empty to use ignition_base_dir)
|
|
'element_mappings': [] # List of {svg_id: 'id', scada_type: 'type', scada_path: 'path'}
|
|
}
|
|
|
|
class ConfigManager:
|
|
"""
|
|
Configuration Manager class for handling configuration persistence.
|
|
|
|
Loads and saves configuration options to a JSON file.
|
|
Auto-creates the configuration file with defaults if it doesn't exist.
|
|
Includes logic for backward compatibility with older formats.
|
|
"""
|
|
|
|
def __init__(self, config_file="config.json"):
|
|
"""
|
|
Initialize the Configuration Manager.
|
|
|
|
Args:
|
|
config_file (str): The name of the configuration file.
|
|
It will be stored in the application's directory.
|
|
"""
|
|
# Store only the filename, calculate full path dynamically
|
|
self.config_filename = config_file
|
|
self.config_file_path = os.path.join(get_application_path(), self.config_filename)
|
|
print(f"DEBUG: Config file path: {self.config_file_path}") # Debug output
|
|
self.initialize_config_file() # Ensure file/dir exists
|
|
|
|
def _get_config_path(self):
|
|
"""Returns the full path to the config file."""
|
|
# Recalculate in case the application path logic changes
|
|
return os.path.join(get_application_path(), self.config_filename)
|
|
|
|
def initialize_config_file(self):
|
|
"""
|
|
Create the configuration file with default values if it doesn't exist.
|
|
Ensures the containing directory exists.
|
|
"""
|
|
config_path = self._get_config_path()
|
|
config_dir = os.path.dirname(config_path)
|
|
|
|
try:
|
|
# Create directory if it doesn't exist
|
|
if config_dir and not os.path.exists(config_dir):
|
|
os.makedirs(config_dir)
|
|
print(f"Created config directory: {config_dir}")
|
|
|
|
# Create default config file if it doesn't exist
|
|
if not os.path.exists(config_path):
|
|
print(f"Config file not found at {config_path}. Creating default config...")
|
|
# Use the save method with default config to create the file
|
|
self.save_config(DEFAULT_CONFIG)
|
|
|
|
except Exception as e:
|
|
print(f"Error initializing config file/directory: {e}")
|
|
|
|
def get_config(self):
|
|
"""
|
|
Load configuration from the file.
|
|
|
|
Returns:
|
|
dict: The loaded (and potentially updated for compatibility) configuration dictionary.
|
|
Returns DEFAULT_CONFIG if loading fails.
|
|
"""
|
|
config_path = self._get_config_path()
|
|
try:
|
|
if os.path.exists(config_path):
|
|
with open(config_path, 'r', encoding='utf-8') as f:
|
|
config = json.load(f)
|
|
# Ensure compatibility and update format
|
|
config = self._ensure_backward_compatibility(config)
|
|
config = self._update_config_format(config)
|
|
# Merge with defaults to ensure all keys exist
|
|
# Loaded values take precedence over defaults
|
|
merged_config = DEFAULT_CONFIG.copy()
|
|
merged_config.update(config) # Overwrite defaults with loaded values
|
|
return merged_config
|
|
else:
|
|
print(f"Configuration file not found: {config_path}. Returning default config.")
|
|
return DEFAULT_CONFIG.copy() # Return a copy of defaults
|
|
except json.JSONDecodeError as e:
|
|
print(f"Error decoding JSON from config file {config_path}: {e}")
|
|
print("Using default configuration.")
|
|
# Optionally back up corrupted file
|
|
# os.rename(config_path, config_path + ".corrupted")
|
|
return DEFAULT_CONFIG.copy()
|
|
except Exception as e:
|
|
print(f"Error loading configuration from {config_path}: {e}")
|
|
return DEFAULT_CONFIG.copy()
|
|
|
|
def save_config(self, config):
|
|
"""
|
|
Save configuration to the file.
|
|
|
|
Args:
|
|
config (dict): The configuration dictionary to save.
|
|
|
|
Returns:
|
|
bool: True if saved successfully, False otherwise.
|
|
"""
|
|
config_path = self._get_config_path()
|
|
try:
|
|
# Ensure the directory exists before writing
|
|
config_dir = os.path.dirname(config_path)
|
|
if config_dir and not os.path.exists(config_dir):
|
|
os.makedirs(config_dir)
|
|
|
|
# Write the config file
|
|
with open(config_path, 'w', encoding='utf-8') as f:
|
|
json.dump(config, f, indent=4)
|
|
# print(f"Configuration saved to: {config_path}") # Less verbose logging
|
|
return True
|
|
except TypeError as e:
|
|
print(f"Error saving configuration: Data not JSON serializable - {e}")
|
|
print(f"Problematic config data (partial): {str(config)[:500]}")
|
|
return False
|
|
except Exception as e:
|
|
print(f"Error saving configuration to {config_path}: {e}")
|
|
return False
|
|
|
|
def _ensure_backward_compatibility(self, config):
|
|
"""Ensure backward compatibility with older config formats."""
|
|
# Check if the new 'element_mappings' structure exists and is populated.
|
|
# If not, try to convert from older, separate mapping dictionaries.
|
|
if 'element_mappings' not in config or not isinstance(config['element_mappings'], list) or not config['element_mappings']:
|
|
print("Attempting conversion from older config format...")
|
|
element_mappings = []
|
|
# Define default values used during conversion
|
|
default_type = config.get('type', 'ia.display.view') # Old general type
|
|
default_props_path = config.get('path', 'Symbol-Views/Equipment-Views/Status') # Old general path
|
|
default_width = config.get('width', 14)
|
|
default_height = config.get('height', 14)
|
|
|
|
# Get old mapping dictionaries safely (default to empty dict)
|
|
element_type_mapping = config.get('element_type_mapping', {})
|
|
element_props_mapping = config.get('element_props_mapping', {})
|
|
element_label_prefix_mapping = config.get('element_label_prefix_mapping', {})
|
|
element_size_mapping = config.get('element_size_mapping', {})
|
|
|
|
# Keep track of SVG types processed to avoid duplicates from different old sources
|
|
processed_svg_types = set()
|
|
|
|
# Combine all known SVG types from the old mappings
|
|
all_svg_types = (set(element_type_mapping.keys()) |
|
|
set(element_props_mapping.keys()) |
|
|
set(element_label_prefix_mapping.keys()) |
|
|
set(element_size_mapping.keys()))
|
|
|
|
for svg_type in all_svg_types:
|
|
if not svg_type: continue # Skip empty keys
|
|
|
|
# Create entries for specific label prefixes if they exist
|
|
if svg_type in element_label_prefix_mapping:
|
|
prefix = element_label_prefix_mapping[svg_type]
|
|
if prefix: # Only if prefix is non-empty
|
|
mapping_key = (svg_type, prefix)
|
|
if mapping_key not in processed_svg_types:
|
|
width, height = self._get_size_from_old_mapping(element_size_mapping, svg_type, default_width, default_height)
|
|
mapping = {
|
|
'svg_type': svg_type,
|
|
'element_type': element_type_mapping.get(svg_type, default_type),
|
|
'label_prefix': prefix,
|
|
'props_path': element_props_mapping.get(svg_type, default_props_path),
|
|
'width': width,
|
|
'height': height,
|
|
'x_offset': 0, # Assume 0 offset in old format
|
|
'y_offset': 0,
|
|
'final_prefix': '', # No equivalent in old format
|
|
'final_suffix': ''
|
|
}
|
|
element_mappings.append(mapping)
|
|
processed_svg_types.add(mapping_key)
|
|
|
|
# Create entry for the SVG type without a label prefix (default)
|
|
mapping_key = (svg_type, "")
|
|
if mapping_key not in processed_svg_types:
|
|
width, height = self._get_size_from_old_mapping(element_size_mapping, svg_type, default_width, default_height)
|
|
mapping = {
|
|
'svg_type': svg_type,
|
|
'element_type': element_type_mapping.get(svg_type, default_type),
|
|
'label_prefix': "", # Empty prefix for the general case
|
|
'props_path': element_props_mapping.get(svg_type, default_props_path),
|
|
'width': width,
|
|
'height': height,
|
|
'x_offset': 0,
|
|
'y_offset': 0,
|
|
'final_prefix': '',
|
|
'final_suffix': ''
|
|
}
|
|
element_mappings.append(mapping)
|
|
processed_svg_types.add(mapping_key)
|
|
|
|
# Update the config with the new list format
|
|
config['element_mappings'] = element_mappings
|
|
print(f"Converted {len(element_mappings)} mappings to new format.")
|
|
|
|
# Optionally remove old keys after conversion (or keep for safety)
|
|
# keys_to_remove = ['type', 'path', 'width', 'height', 'element_type_mapping', ...]
|
|
# for key in keys_to_remove:
|
|
# config.pop(key, None)
|
|
|
|
return config
|
|
|
|
def _get_size_from_old_mapping(self, size_mapping, svg_type, default_w, default_h):
|
|
"""Helper to extract size from the old element_size_mapping format."""
|
|
if svg_type in size_mapping and isinstance(size_mapping[svg_type], dict):
|
|
width = size_mapping[svg_type].get('width', default_w)
|
|
height = size_mapping[svg_type].get('height', default_h)
|
|
# Basic validation
|
|
try: width = int(width)
|
|
except: width = default_w
|
|
try: height = int(height)
|
|
except: height = default_h
|
|
return width, height
|
|
return default_w, default_h
|
|
|
|
def _update_config_format(self, config):
|
|
"""
|
|
Update config format to ensure all required fields exist in element mappings.
|
|
Also ensures top-level keys from DEFAULT_CONFIG exist.
|
|
"""
|
|
# Ensure all top-level keys from DEFAULT_CONFIG exist
|
|
for key, default_value in DEFAULT_CONFIG.items():
|
|
if key not in config:
|
|
print(f"Adding missing config key: '{key}'")
|
|
config[key] = default_value
|
|
|
|
# Ensure 'element_mappings' exists and is a list
|
|
if 'element_mappings' not in config or not isinstance(config['element_mappings'], list):
|
|
print("Reinitializing 'element_mappings' list.")
|
|
config['element_mappings'] = DEFAULT_CONFIG['element_mappings'] # Use default
|
|
|
|
# Check and update each mapping dictionary within the list
|
|
updated_mappings = []
|
|
default_mapping = DEFAULT_CONFIG['element_mappings'][0] if DEFAULT_CONFIG['element_mappings'] else {}
|
|
|
|
for i, mapping in enumerate(config['element_mappings']):
|
|
if not isinstance(mapping, dict):
|
|
print(f"Warning: Skipping non-dictionary item in element_mappings at index {i}")
|
|
continue
|
|
|
|
updated_mapping = mapping.copy() # Work on a copy
|
|
missing_fields = False
|
|
|
|
# Check for essential fields and add defaults if missing
|
|
for field, default_val in default_mapping.items():
|
|
if field not in updated_mapping:
|
|
print(f"Adding missing field '{field}' to mapping for SVG type '{updated_mapping.get('svg_type', 'UNKNOWN')}'")
|
|
updated_mapping[field] = default_val
|
|
missing_fields = True
|
|
|
|
# Optionally, perform type validation/conversion here (e.g., ensure width/height are numbers)
|
|
for field in ['width', 'height', 'x_offset', 'y_offset']:
|
|
try:
|
|
if field in updated_mapping:
|
|
# Convert to int, handle potential errors
|
|
updated_mapping[field] = int(updated_mapping[field])
|
|
except (ValueError, TypeError):
|
|
print(f"Warning: Invalid value for '{field}' in mapping '{updated_mapping.get('svg_type')}'. Setting default.")
|
|
updated_mapping[field] = default_mapping.get(field, 0) # Default numeric to 0 or from DEFAULT_CONFIG
|
|
|
|
updated_mappings.append(updated_mapping)
|
|
|
|
# if missing_fields:
|
|
# print(f"Updated mapping: {updated_mapping}")
|
|
|
|
# Replace the old list with the updated one
|
|
config['element_mappings'] = updated_mappings
|
|
|
|
return config
|
|
|
|
# Example usage (optional, for testing)
|
|
# if __name__ == "__main__":
|
|
# manager = ConfigManager("test_config.json")
|
|
# print("--- Initializing ---")
|
|
# initial_config = manager.get_config()
|
|
# print("Initial config:", json.dumps(initial_config, indent=2))
|
|
#
|
|
# # Modify config
|
|
# initial_config['project_title'] = "Updated Project Title"
|
|
# initial_config['element_mappings'].append({
|
|
# "svg_type": "circle",
|
|
# "element_type": "ia.display.led_display",
|
|
# "label_prefix": "LED_",
|
|
# "props_path": "Symbols/LED",
|
|
# "width": 20,
|
|
# "height": 20,
|
|
# "x_offset": -10,
|
|
# "y_offset": -10,
|
|
# "final_prefix": "PFX_",
|
|
# "final_suffix": "_SFX"
|
|
# })
|
|
#
|
|
# print("\n--- Saving Modified Config ---")
|
|
# save_success = manager.save_config(initial_config)
|
|
# print(f"Save successful: {save_success}")
|
|
#
|
|
# print("\n--- Reloading Config ---")
|
|
# reloaded_config = manager.get_config()
|
|
# print("Reloaded config:", json.dumps(reloaded_config, indent=2))
|
|
#
|
|
# # Test backward compatibility (create a dummy old config)
|
|
# old_config_path = manager._get_config_path() + ".old"
|
|
# old_config_data = {
|
|
# "file_path": "/path/to/old.svg",
|
|
# "project_title": "Old Project",
|
|
# "element_type_mapping": {"rect": "ia.display.rect", "path": "ia.display.path"},
|
|
# "element_label_prefix_mapping": {"rect": "R_"}
|
|
# }
|
|
# with open(old_config_path, 'w') as f:
|
|
# json.dump(old_config_data, f, indent=4)
|
|
#
|
|
# print("\n--- Testing Backward Compatibility --- ")
|
|
# old_manager = ConfigManager(os.path.basename(old_config_path))
|
|
# loaded_old_config = old_manager.get_config()
|
|
# print("Loaded and converted old config:", json.dumps(loaded_old_config, indent=2))
|
|
#
|
|
# # Clean up test files
|
|
# # os.remove(manager._get_config_path())
|
|
# # os.remove(old_config_path) |