diff --git a/TasmotaManager.py b/TasmotaManager.py index 09285af..371ec76 100644 --- a/TasmotaManager.py +++ b/TasmotaManager.py @@ -13,6 +13,7 @@ from configuration import ConfigurationManager from console_settings import ConsoleSettingsManager from unknown_devices import UnknownDeviceProcessor from reporting import ReportGenerator +from device_diff import DeviceComparison def setup_logging(debug: bool = False) -> logging.Logger: @@ -307,6 +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') args = parser.parse_args() @@ -334,6 +337,35 @@ def main(): unknown_processor = UnknownDeviceProcessor(config, config_manager, logger) report_gen = ReportGenerator(config, discovery, logger) + # Handle device comparison mode + if args.diff: + device_comp = DeviceComparison(logger) + + # Get devices list to resolve names/IPs + devices = discovery.get_tasmota_devices() + + # 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]}") + 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 + + # Compare devices + comparison = device_comp.compare_devices( + device1['ip'], device1['name'], + device2['ip'], device2['name'] + ) + + # Print report + device_comp.print_comparison_report(comparison) + return 0 + # Handle hostname report mode if args.unifi_hostname_report: report_gen.generate_unifi_hostname_report() diff --git a/device_diff.py b/device_diff.py new file mode 100644 index 0000000..6b3d5f6 --- /dev/null +++ b/device_diff.py @@ -0,0 +1,250 @@ +"""Device comparison and diagnostics module.""" + +import logging +from typing import Dict, List, Tuple, Optional +from utils import send_tasmota_command + + +class DeviceComparison: + """Compare configuration between two Tasmota devices.""" + + def __init__(self, logger: Optional[logging.Logger] = None): + """ + Initialize device comparison. + + Args: + logger: Optional logger instance + """ + self.logger = logger or logging.getLogger(__name__) + + def get_device_full_status(self, device_ip: str, device_name: str) -> Dict: + """ + Get complete device status and configuration. + + Args: + device_ip: Device IP address + device_name: Device name for logging + + Returns: + Dictionary with all device status information + """ + self.logger.info(f"Querying full status from {device_name} ({device_ip})") + + device_info = { + 'name': device_name, + 'ip': device_ip, + 'firmware': {}, + 'network': {}, + 'mqtt': {}, + 'setoptions': {}, + 'rules': {}, + 'gpio': {}, + 'other': {} + } + + # Get Status 0 (all status) + result, success = send_tasmota_command(device_ip, "Status%200", timeout=10, logger=self.logger) + if success and result: + # Extract firmware info + if 'StatusFWR' in result: + device_info['firmware'] = result['StatusFWR'] + + # Extract network info + if 'StatusNET' in result: + device_info['network'] = result['StatusNET'] + + # Extract MQTT info + if 'StatusMQT' in result: + device_info['mqtt'] = result['StatusMQT'] + + # Extract basic status + if 'Status' in result: + device_info['other']['status'] = result['Status'] + + # Get all SetOptions (0-150) + self.logger.debug(f"Querying SetOptions from {device_name}") + for i in range(0, 151): + result, success = send_tasmota_command(device_ip, f"SetOption{i}", timeout=5, logger=self.logger) + if success and result: + key = f"SetOption{i}" + if key in result: + device_info['setoptions'][key] = result[key] + + # Get Rules (1-3) + self.logger.debug(f"Querying Rules from {device_name}") + for i in range(1, 4): + result, success = send_tasmota_command(device_ip, f"Rule{i}%204", timeout=5, logger=self.logger) + if success and result: + key = f"Rule{i}" + if key in result: + device_info['rules'][key] = result[key] + + # Get other important settings + for cmd in ['ButtonDebounce', 'SwitchDebounce', 'Template', 'Module']: + result, success = send_tasmota_command(device_ip, cmd, timeout=5, logger=self.logger) + if success and result: + device_info['other'][cmd] = result + + return device_info + + def compare_devices(self, device1_ip: str, device1_name: str, + device2_ip: str, device2_name: str) -> Dict: + """ + Compare two devices and return differences. + + Args: + device1_ip: First device IP + device1_name: First device name + device2_ip: Second device IP + device2_name: Second device name + + Returns: + Dictionary with comparison results + """ + self.logger.info(f"Comparing {device1_name} vs {device2_name}") + + # Get full status from both devices + device1 = self.get_device_full_status(device1_ip, device1_name) + device2 = self.get_device_full_status(device2_ip, device2_name) + + # Compare and find differences + differences = { + 'device1': {'name': device1_name, 'ip': device1_ip}, + 'device2': {'name': device2_name, 'ip': device2_ip}, + 'firmware': self._compare_section(device1['firmware'], device2['firmware']), + 'network': self._compare_section(device1['network'], device2['network']), + 'mqtt': self._compare_section(device1['mqtt'], device2['mqtt']), + 'setoptions': self._compare_section(device1['setoptions'], device2['setoptions']), + 'rules': self._compare_section(device1['rules'], device2['rules']), + 'other': self._compare_section(device1['other'], device2['other']) + } + + return differences + + def _compare_section(self, section1: Dict, section2: Dict) -> List[Dict]: + """ + Compare two configuration sections. + + Args: + section1: First device section + section2: Second device section + + Returns: + List of differences + """ + differences = [] + + # Get all keys from both sections + all_keys = set(section1.keys()) | set(section2.keys()) + + for key in sorted(all_keys): + val1 = section1.get(key) + val2 = section2.get(key) + + if val1 != val2: + differences.append({ + 'key': key, + 'device1_value': val1, + 'device2_value': val2 + }) + + return differences + + def print_comparison_report(self, comparison: Dict) -> None: + """ + Print human-readable comparison report. + + Args: + comparison: Comparison results dictionary + """ + print("\n" + "=" * 80) + print("DEVICE COMPARISON REPORT") + print("=" * 80) + + device1 = comparison['device1'] + device2 = comparison['device2'] + + print(f"\nDevice 1: {device1['name']} ({device1['ip']})") + print(f"Device 2: {device2['name']} ({device2['ip']})") + + # Print firmware info + print("\n" + "-" * 80) + print("FIRMWARE DIFFERENCES") + print("-" * 80) + + if comparison['firmware']: + self._print_differences(comparison['firmware'], device1['name'], device2['name']) + else: + print("No differences found") + + # Print network differences + print("\n" + "-" * 80) + print("NETWORK DIFFERENCES") + print("-" * 80) + + if comparison['network']: + self._print_differences(comparison['network'], device1['name'], device2['name']) + else: + print("No differences found") + + # Print MQTT differences + print("\n" + "-" * 80) + print("MQTT DIFFERENCES") + print("-" * 80) + + if comparison['mqtt']: + self._print_differences(comparison['mqtt'], device1['name'], device2['name']) + else: + print("No differences found") + + # Print SetOption differences + print("\n" + "-" * 80) + print("SETOPTION DIFFERENCES") + print("-" * 80) + + if comparison['setoptions']: + self._print_differences(comparison['setoptions'], device1['name'], device2['name']) + else: + print("No differences found") + + # Print Rule differences + print("\n" + "-" * 80) + print("RULE DIFFERENCES") + print("-" * 80) + + if comparison['rules']: + self._print_differences(comparison['rules'], device1['name'], device2['name']) + else: + print("No differences found") + + # Print other differences + print("\n" + "-" * 80) + print("OTHER CONFIGURATION DIFFERENCES") + print("-" * 80) + + if comparison['other']: + self._print_differences(comparison['other'], device1['name'], device2['name']) + else: + print("No differences found") + + print("\n" + "=" * 80) + print("END OF REPORT") + print("=" * 80 + "\n") + + def _print_differences(self, differences: List[Dict], device1_name: str, device2_name: str) -> None: + """ + Print list of differences in readable format. + + Args: + differences: List of difference dictionaries + device1_name: First device name + device2_name: Second device name + """ + for diff in differences: + key = diff['key'] + val1 = diff['device1_value'] + val2 = diff['device2_value'] + + print(f"\n{key}:") + print(f" {device1_name:20} = {val1}") + print(f" {device2_name:20} = {val2}")