2025-09-11 00:08:34 +04:00

364 lines
16 KiB
Python

#!/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}")