TasmotaManager/get_device_hostname_implementation_summary.md
Mike Geppert 126cd39555 Major code improvements and bug fixes:
1. Restructured configuration: Moved config_other and console to top level
2. Added common _match_pattern function for regex pattern matching
3. Implemented Unifi Hostname bug fix in is_hostname_unknown
4. Created common get_device_hostname function to eliminate code duplication
5. Added comprehensive test scripts for all new functionality
6. Added detailed documentation for all changes
2025-08-08 19:04:33 -05:00

7.3 KiB

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:

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

# 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

# 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

# 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

# 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.