# Common Function for Device Hostname Retrieval ## Issue Description The TasmotaManager codebase had multiple locations that retrieved a device's hostname from a Tasmota device using similar code patterns. This duplication made the code harder to maintain and increased the risk of inconsistencies if one implementation was updated but not the others. ## Solution A common function `get_device_hostname` was implemented to centralize the hostname retrieval logic, eliminating code duplication and ensuring consistent error handling and logging across the codebase. ## Changes Made ### 1. Common Function Implementation A new function `get_device_hostname` was added to the TasmotaManager class: ```python def get_device_hostname(self, ip: str, device_name: str = None, timeout: int = 5, log_level: str = 'debug') -> tuple: """Retrieve the hostname from a Tasmota device. This function makes an HTTP request to a Tasmota device to retrieve its self-reported hostname using the Status 5 command. It handles error conditions and provides consistent logging. Args: ip: The IP address of the device device_name: Optional name of the device for logging purposes timeout: Timeout for the HTTP request in seconds (default: 5) log_level: The logging level to use ('debug', 'info', 'warning', 'error'). Default is 'debug'. Returns: tuple: (hostname, success) - hostname: The device's self-reported hostname, or empty string if not found - success: Boolean indicating whether the hostname was successfully retrieved """ # Set up logging based on the specified level log_func = getattr(self.logger, log_level) # Use device_name in logs if provided, otherwise use IP device_id = device_name if device_name else ip hostname = "" success = False try: # Log attempt to retrieve hostname log_func(f"Retrieving hostname for {device_id} at {ip}") # Make HTTP request to the device url = f"http://{ip}/cm?cmnd=Status%205" response = requests.get(url, timeout=timeout) # Check if response is successful if response.status_code == 200: try: # Parse JSON response status_data = response.json() # Extract hostname from response hostname = status_data.get('StatusNET', {}).get('Hostname', '') if hostname: log_func(f"Successfully retrieved hostname for {device_id}: {hostname}") success = True else: log_func(f"No hostname found in response for {device_id}") except ValueError: log_func(f"Failed to parse JSON response from {device_id}") else: log_func(f"Failed to get hostname for {device_id}: HTTP {response.status_code}") except requests.exceptions.RequestException as e: log_func(f"Error retrieving hostname for {device_id}: {str(e)}") return hostname, success ``` ### 2. Updated Locations Four locations in the codebase were updated to use the new common function: #### a. `is_hostname_unknown` Function ```python # Get the device's self-reported hostname using the common function device_reported_hostname, success = self.get_device_hostname(ip, hostname, timeout=5, log_level='debug') if success: # Check if the self-reported hostname also matches unknown patterns device_hostname_matches_unknown = False for pattern in patterns: if self._match_pattern(device_reported_hostname.lower(), pattern, match_entire_string=False): device_hostname_matches_unknown = True self.logger.debug(f"Device's self-reported hostname '{device_reported_hostname}' matches unknown pattern: {pattern}") break ``` #### b. `get_tasmota_devices` Method ```python # Get the device's self-reported hostname using the common function device_reported_hostname, success = self.get_device_hostname(device_ip, device_name, timeout=5, log_level='debug') if success: # Check if the self-reported hostname also matches unknown patterns device_hostname_matches_unknown = False for pattern in unknown_patterns: if self._match_pattern(device_reported_hostname.lower(), pattern, match_entire_string=False): device_hostname_matches_unknown = True self.logger.debug(f"Device's self-reported hostname '{device_reported_hostname}' matches unknown pattern: {pattern}") break ``` #### c. `process_single_device` Method ```python # Get the device's self-reported hostname using the common function device_reported_hostname, success = self.get_device_hostname(device_ip, device_name, timeout=5, log_level='info') if success and device_reported_hostname: # Check if the self-reported hostname also matches unknown patterns device_hostname_matches_unknown = False for pattern in unknown_patterns: if self._match_pattern(device_reported_hostname.lower(), pattern, match_entire_string=False): device_hostname_matches_unknown = True self.logger.info(f"Device's self-reported hostname '{device_reported_hostname}' matches unknown pattern: {pattern}") break ``` #### d. Device Details Collection ```python # Get Status 5 for network info using the common function hostname, hostname_success = self.get_device_hostname(ip, name, timeout=5, log_level='info') # Create a network_data structure for backward compatibility network_data = {"StatusNET": {"Hostname": hostname if hostname_success else "Unknown"}} ``` ### 3. Comprehensive Testing A test file `test_get_device_hostname.py` was created to verify that the `get_device_hostname` function works correctly. The tests cover: 1. Successful hostname retrieval 2. Empty hostname in response 3. Missing hostname in response 4. Invalid JSON response 5. Non-200 status code 6. Connection error 7. Timeout error 8. Custom timeout parameter 9. Device name parameter 10. Log level parameter All tests pass, confirming that the function handles all scenarios correctly. ## Benefits The implementation of the common `get_device_hostname` function provides several benefits: 1. **Code Reuse**: Eliminates duplicated code for retrieving a device's hostname, reducing the codebase size and complexity. 2. **Consistency**: Ensures consistent error handling and logging across the codebase, making the behavior more predictable and easier to understand. 3. **Maintainability**: Makes it easier to update the hostname retrieval logic in one place, rather than having to update multiple locations. 4. **Readability**: Makes the code more concise and easier to understand, as the hostname retrieval logic is now encapsulated in a well-named function. 5. **Flexibility**: Provides options for customizing timeout and logging level, making the function more versatile for different use cases. 6. **Reliability**: Comprehensive testing ensures that the function works correctly in all scenarios, including error conditions. ## Conclusion The implementation of the common `get_device_hostname` function has successfully eliminated code duplication, improved maintainability, and ensured consistent error handling and logging across the codebase. The function is well-tested and provides a flexible, reliable way to retrieve a device's hostname from a Tasmota device.