Add single-device comparison to config file
- Modified --diff to accept 1 or 2 device arguments - With 1 device: compares device config vs expected config from file - With 2 devices: compares devices to each other (existing behavior) - Shows which SetOptions and Rules don't match configuration - Normalizes ON/OFF vs 1/0 for proper comparison - Helps diagnose why console settings aren't applying correctly
This commit is contained in:
parent
72b21bc838
commit
7f00bb8d7b
@ -308,8 +308,8 @@ def main():
|
||||
help='Generate UniFi hostname comparison report')
|
||||
parser.add_argument('--Device', type=str,
|
||||
help='Process single device by IP or hostname')
|
||||
parser.add_argument('--diff', nargs=2, metavar=('DEVICE1', 'DEVICE2'),
|
||||
help='Compare two devices and show configuration differences')
|
||||
parser.add_argument('--diff', nargs='+', metavar='DEVICE',
|
||||
help='Compare devices: --diff DEVICE (vs config) or --diff DEVICE1 DEVICE2 (vs each other)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@ -339,7 +339,11 @@ def main():
|
||||
|
||||
# Handle device comparison mode
|
||||
if args.diff:
|
||||
device_comp = DeviceComparison(logger)
|
||||
if len(args.diff) < 1 or len(args.diff) > 2:
|
||||
logger.error("--diff requires 1 or 2 device arguments")
|
||||
return 1
|
||||
|
||||
device_comp = DeviceComparison(config, logger)
|
||||
|
||||
# Get devices list to resolve names/IPs
|
||||
devices = discovery.get_tasmota_devices()
|
||||
@ -347,20 +351,37 @@ def main():
|
||||
# Find device 1
|
||||
device1 = find_device_by_identifier(devices, args.diff[0], logger)
|
||||
if not device1:
|
||||
logger.error(f"Device 1 not found: {args.diff[0]}")
|
||||
logger.error(f"Device not found: {args.diff[0]}")
|
||||
return 1
|
||||
|
||||
# Find device 2
|
||||
device2 = find_device_by_identifier(devices, args.diff[1], logger)
|
||||
if not device2:
|
||||
logger.error(f"Device 2 not found: {args.diff[1]}")
|
||||
return 1
|
||||
if len(args.diff) == 1:
|
||||
# Single device: compare to config file
|
||||
# Get device type from Status
|
||||
from utils import send_tasmota_command
|
||||
result, success = send_tasmota_command(device1['ip'], "Status%200", timeout=10, logger=logger)
|
||||
if not success or not result:
|
||||
logger.error(f"Failed to get device status for {device1['name']}")
|
||||
return 1
|
||||
|
||||
# Compare devices
|
||||
comparison = device_comp.compare_devices(
|
||||
device1['ip'], device1['name'],
|
||||
device2['ip'], device2['name']
|
||||
)
|
||||
device_type = result.get('Status', {}).get('DeviceName', '')
|
||||
if not device_type:
|
||||
logger.error(f"Could not determine device type for {device1['name']}")
|
||||
return 1
|
||||
|
||||
comparison = device_comp.compare_device_to_config(
|
||||
device1['ip'], device1['name'], device_type
|
||||
)
|
||||
else:
|
||||
# Two devices: compare to each other
|
||||
device2 = find_device_by_identifier(devices, args.diff[1], logger)
|
||||
if not device2:
|
||||
logger.error(f"Device 2 not found: {args.diff[1]}")
|
||||
return 1
|
||||
|
||||
comparison = device_comp.compare_devices(
|
||||
device1['ip'], device1['name'],
|
||||
device2['ip'], device2['name']
|
||||
)
|
||||
|
||||
# Print report
|
||||
device_comp.print_comparison_report(comparison)
|
||||
|
||||
164
device_diff.py
164
device_diff.py
@ -9,13 +9,15 @@ from utils import send_tasmota_command
|
||||
class DeviceComparison:
|
||||
"""Compare configuration between two Tasmota devices."""
|
||||
|
||||
def __init__(self, logger: Optional[logging.Logger] = None):
|
||||
def __init__(self, config: Optional[Dict] = None, logger: Optional[logging.Logger] = None):
|
||||
"""
|
||||
Initialize device comparison.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary (for comparing to config file)
|
||||
logger: Optional logger instance
|
||||
"""
|
||||
self.config = config
|
||||
self.logger = logger or logging.getLogger(__name__)
|
||||
|
||||
def get_device_full_status(self, device_ip: str, device_name: str) -> Dict:
|
||||
@ -88,6 +90,166 @@ class DeviceComparison:
|
||||
|
||||
return device_info
|
||||
|
||||
def get_expected_config(self, device_type: str) -> Dict:
|
||||
"""
|
||||
Get expected configuration from config file for a device type.
|
||||
|
||||
Args:
|
||||
device_type: Device type/DeviceName
|
||||
|
||||
Returns:
|
||||
Dictionary with expected configuration
|
||||
"""
|
||||
expected = {
|
||||
'name': 'Expected (from config)',
|
||||
'setoptions': {},
|
||||
'rules': {},
|
||||
'console_set': None
|
||||
}
|
||||
|
||||
if not self.config:
|
||||
return expected
|
||||
|
||||
# Find console_set for this device type
|
||||
device_list = self.config.get('device_list', {})
|
||||
console_set_name = None
|
||||
|
||||
# Try exact match first
|
||||
if device_type in device_list:
|
||||
console_set_name = device_list[device_type].get('console_set')
|
||||
else:
|
||||
# Try case-insensitive match
|
||||
device_type_lower = device_type.lower()
|
||||
for template_name, template_data in device_list.items():
|
||||
if template_name.lower() == device_type_lower:
|
||||
console_set_name = template_data.get('console_set')
|
||||
break
|
||||
|
||||
if not console_set_name:
|
||||
self.logger.warning(f"No console_set found for device type: {device_type}")
|
||||
return expected
|
||||
|
||||
expected['console_set'] = console_set_name
|
||||
|
||||
# Get console commands for this set
|
||||
console_set = self.config.get('console_set', {})
|
||||
commands = console_set.get(console_set_name, [])
|
||||
|
||||
# Parse commands into SetOptions and Rules
|
||||
for command in commands:
|
||||
if not command or not command.strip():
|
||||
continue
|
||||
|
||||
parts = command.split(None, 1)
|
||||
if not parts:
|
||||
continue
|
||||
|
||||
param_name = parts[0]
|
||||
param_value = parts[1] if len(parts) > 1 else ""
|
||||
|
||||
if param_name.startswith('SetOption'):
|
||||
# Store raw value - we'll normalize during comparison
|
||||
expected['setoptions'][param_name] = param_value
|
||||
elif param_name.lower().startswith('rule'):
|
||||
# For rules, just store the rule text
|
||||
expected['rules'][param_name] = {
|
||||
'State': 'ON', # Rules should be enabled
|
||||
'Once': 'OFF', # Should not be Once
|
||||
'Rules': param_value
|
||||
}
|
||||
|
||||
return expected
|
||||
|
||||
def compare_device_to_config(self, device_ip: str, device_name: str, device_type: str) -> Dict:
|
||||
"""
|
||||
Compare a device's actual configuration to expected config from file.
|
||||
|
||||
Args:
|
||||
device_ip: Device IP address
|
||||
device_name: Device name
|
||||
device_type: Device type/DeviceName
|
||||
|
||||
Returns:
|
||||
Dictionary with comparison results
|
||||
"""
|
||||
self.logger.info(f"Comparing {device_name} vs expected config for {device_type}")
|
||||
|
||||
# Get actual device config
|
||||
device = self.get_device_full_status(device_ip, device_name)
|
||||
|
||||
# Get expected config
|
||||
expected = self.get_expected_config(device_type)
|
||||
|
||||
# Compare only SetOptions and Rules that are in config
|
||||
setoption_diffs = []
|
||||
for key, expected_value in expected['setoptions'].items():
|
||||
actual_value = device['setoptions'].get(key)
|
||||
|
||||
# Normalize values for comparison (0/1 vs OFF/ON)
|
||||
def normalize(val):
|
||||
if val is None:
|
||||
return None
|
||||
val_str = str(val).upper()
|
||||
if val_str in ['0', 'OFF', 'FALSE']:
|
||||
return 'OFF'
|
||||
elif val_str in ['1', 'ON', 'TRUE']:
|
||||
return 'ON'
|
||||
return str(val)
|
||||
|
||||
if normalize(actual_value) != normalize(expected_value):
|
||||
setoption_diffs.append({
|
||||
'key': key,
|
||||
'device1_value': actual_value,
|
||||
'device2_value': expected_value
|
||||
})
|
||||
|
||||
rule_diffs = []
|
||||
for key, expected_rule in expected['rules'].items():
|
||||
# Normalize key (rule1 vs Rule1)
|
||||
rule_key = key[0].upper() + key[1:] # Capitalize first letter
|
||||
actual_rule = device['rules'].get(rule_key, {})
|
||||
|
||||
# Compare rule content
|
||||
rules_match = True
|
||||
if not actual_rule:
|
||||
rules_match = False
|
||||
else:
|
||||
# Compare State, Once, and Rules content
|
||||
expected_state = expected_rule.get('State', 'ON')
|
||||
actual_state = actual_rule.get('State', 'OFF')
|
||||
if actual_state != expected_state:
|
||||
rules_match = False
|
||||
|
||||
expected_once = expected_rule.get('Once', 'OFF')
|
||||
actual_once = actual_rule.get('Once', 'ON')
|
||||
if actual_once != expected_once:
|
||||
rules_match = False
|
||||
|
||||
expected_rules = expected_rule.get('Rules', '')
|
||||
actual_rules = actual_rule.get('Rules', '')
|
||||
if actual_rules.strip() != expected_rules.strip():
|
||||
rules_match = False
|
||||
|
||||
if not rules_match:
|
||||
rule_diffs.append({
|
||||
'key': rule_key,
|
||||
'device1_value': actual_rule if actual_rule else {},
|
||||
'device2_value': expected_rule
|
||||
})
|
||||
|
||||
differences = {
|
||||
'device1': {'name': device_name, 'ip': device_ip},
|
||||
'device2': {'name': f"Expected ({expected['console_set']})", 'ip': 'config file'},
|
||||
'firmware': [],
|
||||
'network': [],
|
||||
'mqtt': [],
|
||||
'setoptions': setoption_diffs,
|
||||
'rules': rule_diffs,
|
||||
'other': []
|
||||
}
|
||||
|
||||
return differences
|
||||
|
||||
def compare_devices(self, device1_ip: str, device1_name: str,
|
||||
device2_ip: str, device2_name: str) -> Dict:
|
||||
"""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user