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')
|
help='Generate UniFi hostname comparison report')
|
||||||
parser.add_argument('--Device', type=str,
|
parser.add_argument('--Device', type=str,
|
||||||
help='Process single device by IP or hostname')
|
help='Process single device by IP or hostname')
|
||||||
parser.add_argument('--diff', nargs=2, metavar=('DEVICE1', 'DEVICE2'),
|
parser.add_argument('--diff', nargs='+', metavar='DEVICE',
|
||||||
help='Compare two devices and show configuration differences')
|
help='Compare devices: --diff DEVICE (vs config) or --diff DEVICE1 DEVICE2 (vs each other)')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@ -339,7 +339,11 @@ def main():
|
|||||||
|
|
||||||
# Handle device comparison mode
|
# Handle device comparison mode
|
||||||
if args.diff:
|
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
|
# Get devices list to resolve names/IPs
|
||||||
devices = discovery.get_tasmota_devices()
|
devices = discovery.get_tasmota_devices()
|
||||||
@ -347,20 +351,37 @@ def main():
|
|||||||
# Find device 1
|
# Find device 1
|
||||||
device1 = find_device_by_identifier(devices, args.diff[0], logger)
|
device1 = find_device_by_identifier(devices, args.diff[0], logger)
|
||||||
if not device1:
|
if not device1:
|
||||||
logger.error(f"Device 1 not found: {args.diff[0]}")
|
logger.error(f"Device not found: {args.diff[0]}")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# Find device 2
|
if len(args.diff) == 1:
|
||||||
device2 = find_device_by_identifier(devices, args.diff[1], logger)
|
# Single device: compare to config file
|
||||||
if not device2:
|
# Get device type from Status
|
||||||
logger.error(f"Device 2 not found: {args.diff[1]}")
|
from utils import send_tasmota_command
|
||||||
return 1
|
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
|
device_type = result.get('Status', {}).get('DeviceName', '')
|
||||||
comparison = device_comp.compare_devices(
|
if not device_type:
|
||||||
device1['ip'], device1['name'],
|
logger.error(f"Could not determine device type for {device1['name']}")
|
||||||
device2['ip'], device2['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
|
# Print report
|
||||||
device_comp.print_comparison_report(comparison)
|
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:
|
class DeviceComparison:
|
||||||
"""Compare configuration between two Tasmota devices."""
|
"""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.
|
Initialize device comparison.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
config: Configuration dictionary (for comparing to config file)
|
||||||
logger: Optional logger instance
|
logger: Optional logger instance
|
||||||
"""
|
"""
|
||||||
|
self.config = config
|
||||||
self.logger = logger or logging.getLogger(__name__)
|
self.logger = logger or logging.getLogger(__name__)
|
||||||
|
|
||||||
def get_device_full_status(self, device_ip: str, device_name: str) -> Dict:
|
def get_device_full_status(self, device_ip: str, device_name: str) -> Dict:
|
||||||
@ -88,6 +90,166 @@ class DeviceComparison:
|
|||||||
|
|
||||||
return device_info
|
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,
|
def compare_devices(self, device1_ip: str, device1_name: str,
|
||||||
device2_ip: str, device2_name: str) -> Dict:
|
device2_ip: str, device2_name: str) -> Dict:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user