#!/usr/bin/env python3 """ MCM Pattern Assignment Utilities ================================ Shared utilities for determining parent module assignments based on device name patterns from generator configuration files. This allows flexible assignment of devices to different MCM parent modules based on configurable patterns, supporting both simple contains matching and advanced regex patterns. """ import json import os import re from typing import Optional def determine_parent_module_from_config(device_name: str, config_file_path: str = None, is_network_module: bool = False, has_ip_address: bool = None) -> tuple[str, bool, str, str]: """Determine parent module based on MCM device assignment patterns from config. Args: device_name: Name of the device/module to assign config_file_path: Path to the generator config JSON file. If None, looks for common config files in the project root. is_network_module: Whether this is a network module (deprecated, use has_ip_address) has_ip_address: Whether the module has an IP address (more accurate than is_network_module) Returns: Tuple of (parent_module_name, should_auto_create_parent, slot_number, ip_address) """ # Default fallback default_parent = "SLOT2_EN4TR" default_auto_create = False default_slot = "2" default_ip = "11.200.1.10" # Determine if this is a network module module_has_ip = has_ip_address if has_ip_address is not None else is_network_module # Try to find config file if not provided if config_file_path is None: config_file_path = _find_generator_config() if not config_file_path or not os.path.exists(config_file_path): print(f" No MCM config found for pattern matching, using default parent: {default_parent}") return default_parent, default_auto_create, default_slot, default_ip try: with open(config_file_path, 'r') as f: config = json.load(f) assignment_config = config.get('mcm_device_assignment', {}) if not assignment_config: return default_parent, default_auto_create, default_slot, default_ip pattern_rules = assignment_config.get('pattern_rules', []) fallback_parent = assignment_config.get('fallback_parent', default_parent) fallback_auto_create = assignment_config.get('auto_create_fallback_parent', default_auto_create) fallback_slot = assignment_config.get('fallback_slot', default_slot) fallback_ip = assignment_config.get('fallback_ip', default_ip) # Get EN4TR slot configuration en4tr_config = assignment_config.get('en4tr_slot_config', {}) base_ip = en4tr_config.get('base_ip_pattern', '11.200.1.') ip_offset = en4tr_config.get('ip_start_offset', 10) # Process rules in order (first match wins) for rule in pattern_rules: if not rule.get('enabled', True): continue # Check if rule is for network modules only network_only = rule.get('network_modules_only', False) if network_only and not module_has_ip: continue # Skip this rule for non-network modules device_patterns = rule.get('device_contains_pattern', []) parent_tag = rule.get('parent_tag', default_parent) auto_create = rule.get('auto_create_parent', default_auto_create) slot_number = rule.get('parent_slot', _extract_slot_from_parent_tag(parent_tag)) parent_ip = rule.get('parent_ip', f"{base_ip}{ip_offset + int(slot_number)}") match_mode = rule.get('match_mode', 'contains') # Check if device name matches any patterns if match_mode == 'regex': regex_pattern = rule.get('regex_pattern', '') if regex_pattern and re.search(regex_pattern, device_name, re.IGNORECASE): network_suffix = " (network module)" if network_only else "" print(f" Device '{device_name}'{network_suffix} matched regex pattern '{regex_pattern}', assigned to: {parent_tag} (Slot {slot_number}, IP {parent_ip})") return parent_tag, auto_create, slot_number, parent_ip else: # Default contains matching for pattern in device_patterns: if pattern.upper() in device_name.upper(): network_suffix = " (network module)" if network_only else "" print(f" Device '{device_name}'{network_suffix} matched pattern '{pattern}', assigned to: {parent_tag} (Slot {slot_number}, IP {parent_ip})") return parent_tag, auto_create, slot_number, parent_ip # No patterns matched, use fallback print(f" Device '{device_name}' matched no patterns, using fallback parent: {fallback_parent} (Slot {fallback_slot}, IP {fallback_ip})") return fallback_parent, fallback_auto_create, fallback_slot, fallback_ip except Exception as e: print(f" Error reading MCM config from {config_file_path}: {e}") return default_parent, default_auto_create, default_slot, default_ip def _extract_slot_from_parent_tag(parent_tag: str) -> str: """Extract slot number from parent tag name like SLOT3_EN4TR.""" import re match = re.search(r'SLOT(\d+)_EN4TR', parent_tag, re.IGNORECASE) return match.group(1) if match else "2" def _find_generator_config() -> Optional[str]: """Find generator config file by searching common locations and names.""" # Look for common config files in parent directories # Prioritize SAT9 config first since it's the current project possible_configs = [ "SAT9_generator_config.json", "CNO8_generator_config.json", "MTN6_generator_config.json", "generator_config.json" ] # Check current directory and parent directories current_dir = os.getcwd() for i in range(3): # Check up to 3 levels up for config_name in possible_configs: potential_path = os.path.join(current_dir, config_name) if os.path.exists(potential_path): return potential_path current_dir = os.path.dirname(current_dir) return None def determine_parent_with_fallback(device_name: str, default_parent: str = "SLOT2_EN4TR", config_file_path: str = None, has_ip_address: bool = None) -> tuple[str, bool]: """Determine parent module with custom default fallback. Args: device_name: Name of the device/module to assign default_parent: Custom default parent if no patterns match config_file_path: Path to the generator config JSON file has_ip_address: Whether the module has an IP address Returns: Tuple of (parent_module_name, should_auto_create_parent) """ parent, auto_create, _, _ = determine_parent_module_from_config( device_name, config_file_path, is_network_module=has_ip_address or False, has_ip_address=has_ip_address ) return parent, auto_create def determine_parent_with_slot_info(device_name: str, default_parent: str = "SLOT2_EN4TR", config_file_path: str = None, has_ip_address: bool = None) -> tuple[str, bool, str, str]: """Determine parent module with full slot and IP information. Args: device_name: Name of the device/module to assign default_parent: Custom default parent if no patterns match config_file_path: Path to the generator config JSON file has_ip_address: Whether the module has an IP address Returns: Tuple of (parent_module_name, should_auto_create_parent, slot_number, ip_address) """ return determine_parent_module_from_config( device_name, config_file_path, is_network_module=has_ip_address or False, has_ip_address=has_ip_address ) # Convenience functions for common module types def get_parent_for_m12dr(device_name: str, has_ip_address: bool = None) -> str: """Get parent module for M12DR modules with appropriate default.""" parent, _ = determine_parent_with_fallback(device_name, "SLOT2_EN4TR", has_ip_address=has_ip_address) return parent def get_parent_for_vfd(device_name: str, has_ip_address: bool = None) -> str: """Get parent module for VFD modules with appropriate default.""" parent, _ = determine_parent_with_fallback(device_name, "SLOT2_EN4TR", has_ip_address=has_ip_address) return parent def get_parent_for_extendo(device_name: str, has_ip_address: bool = None) -> str: """Get parent module for Extendo modules with appropriate default.""" parent, _ = determine_parent_with_fallback(device_name, "SLOT2_EN4TR", has_ip_address=has_ip_address) return parent def get_parent_for_apf(device_name: str, has_ip_address: bool = None) -> str: """Get parent module for APF modules with appropriate default.""" parent, _ = determine_parent_with_fallback(device_name, "SLOT2_EN4TR", has_ip_address=has_ip_address) return parent def get_parent_for_zmx(device_name: str, has_ip_address: bool = None) -> str: """Get parent module for ZMX modules with appropriate default.""" parent, _ = determine_parent_with_fallback(device_name, "SLOT2_EN4TR", has_ip_address=has_ip_address) return parent def get_parent_for_turck_hub(device_name: str, has_ip_address: bool = None) -> str: """Get parent module for Turck Hub modules with appropriate default.""" parent, _ = determine_parent_with_fallback(device_name, "SLOT2_EN4TR", has_ip_address=has_ip_address) return parent def get_parent_for_dpm(device_name: str, has_ip_address: bool = None) -> str: """Get parent module for DPM modules with appropriate default.""" parent, _ = determine_parent_with_fallback(device_name, "SLOT2_EN4TR", has_ip_address=has_ip_address) return parent def get_parent_for_sio(device_name: str, has_ip_address: bool = None) -> str: """Get parent module for SIO modules with appropriate default.""" parent, _ = determine_parent_with_fallback(device_name, "SLOT2_EN4TR", has_ip_address=has_ip_address) return parent def get_parent_for_pmm(device_name: str, has_ip_address: bool = None) -> str: """Get parent module for PMM modules with appropriate default.""" parent, _ = determine_parent_with_fallback(device_name, "SLOT2_EN4TR", has_ip_address=has_ip_address) return parent def get_parent_for_lpe(device_name: str, has_ip_address: bool = None) -> str: """Get parent module for LPE modules with appropriate default.""" parent, _ = determine_parent_with_fallback(device_name, "SLOT2_EN4TR", has_ip_address=has_ip_address) return parent def get_parent_for_beacon(device_name: str, has_ip_address: bool = None) -> str: """Get parent module for Beacon modules with appropriate default.""" parent, _ = determine_parent_with_fallback(device_name, "SLOT2_EN4TR", has_ip_address=has_ip_address) return parent def get_parent_for_solenoid(device_name: str, has_ip_address: bool = None) -> str: """Get parent module for Solenoid modules with appropriate default.""" parent, _ = determine_parent_with_fallback(device_name, "SLOT2_EN4TR", has_ip_address=has_ip_address) return parent # Enhanced functions that return both parent and auto-create flag def get_parent_and_create_flag_for_m12dr(device_name: str, has_ip_address: bool = None) -> tuple[str, bool]: """Get parent module and auto-create flag for M12DR modules.""" return determine_parent_with_fallback(device_name, "SLOT2_EN4TR", has_ip_address=has_ip_address) def get_parent_and_create_flag(device_name: str, has_ip_address: bool = None) -> tuple[str, bool]: """Get parent module and auto-create flag for any module type.""" return determine_parent_with_fallback(device_name, "SLOT2_EN4TR", has_ip_address=has_ip_address) # Registry for tracking EN4TR modules that need to be created _required_en4tr_modules = {} # Now stores {module_name: (slot, ip)} def register_required_en4tr(parent_module_name: str, slot_number: str = None, ip_address: str = None): """Register an EN4TR module that needs to be created.""" global _required_en4tr_modules if parent_module_name and parent_module_name.endswith("_EN4TR"): _required_en4tr_modules[parent_module_name] = (slot_number, ip_address) print(f" Registered EN4TR module for auto-creation: {parent_module_name} (Slot {slot_number}, IP {ip_address})") def get_required_en4tr_modules() -> dict: """Get the dictionary of EN4TR modules that need to be created.""" global _required_en4tr_modules return _required_en4tr_modules.copy() def clear_required_en4tr_modules(): """Clear the registry of required EN4TR modules.""" global _required_en4tr_modules _required_en4tr_modules.clear() def check_and_register_en4tr_for_device(device_name: str, has_ip_address: bool = None): """Check pattern matching for a device and register EN4TR if needed.""" parent, should_auto_create, slot_number, ip_address = determine_parent_with_slot_info(device_name, has_ip_address=has_ip_address) if should_auto_create and parent: register_required_en4tr(parent, slot_number, ip_address) return parent def create_en4tr_modules_from_registry(ethernet_base_ip: str = "11.200.1.") -> dict: """Create EN4TR module configs for all registered modules. Args: ethernet_base_ip: Base IP address pattern (e.g., "11.200.1.") - used as fallback only Returns: Dictionary mapping module names to EN4TRModuleConfig objects """ from .en4tr_boilerplate_model import create_en4tr_for_slot, extract_slot_from_name global _required_en4tr_modules created_configs = {} ip_counter = 10 # Fallback IP counter for en4tr_name, (slot_number, ip_address) in _required_en4tr_modules.items(): # Use config-specified slot and IP if available, otherwise extract/generate slot = slot_number if slot_number else extract_slot_from_name(en4tr_name) ip = ip_address if ip_address else f"{ethernet_base_ip}{ip_counter}" config = create_en4tr_for_slot( slot_number=slot, ethernet_address=ip, name=en4tr_name ) created_configs[en4tr_name] = config print(f" Created EN4TR config: {en4tr_name} (Slot {slot}) -> {ip}") # Only increment counter if we used fallback IP if not ip_address: ip_counter += 1 return created_configs # Example usage if __name__ == "__main__": # Test pattern matching with sample device names for slot specification test_devices = [ "VS01C_FIOM18", # Should go to SLOT3_EN4TR (slot 3, IP 11.200.1.12) "VS01B_FIOM1", # Should go to SLOT2_EN4TR (slot 2, IP 11.200.1.11) "OTHER_MODULE" # Should use fallback ] print("Testing slot-based pattern matching:") for device in test_devices: parent, auto_create, slot, ip = determine_parent_module_from_config(device, has_ip_address=True) print(f"Device: {device} -> Parent: {parent}, Slot: {slot}, IP: {ip}, Auto-create: {auto_create}") if auto_create: register_required_en4tr(parent, slot, ip) print("\nRegistered EN4TR modules:") required_modules = get_required_en4tr_modules() for module_name, (slot, ip) in required_modules.items(): print(f" {module_name}: Slot {slot}, IP {ip}") print("\nCreating EN4TR configs from registry:") configs = create_en4tr_modules_from_registry() for name, config in configs.items(): print(f" {name}: Slot {config.slot_number}, IP {config.ethernet_address}")